Wikilivres
frwikibooks
https://fr.wikibooks.org/wiki/Accueil
MediaWiki 1.47.0-wmf.20
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
Event
Event talk
Wikilivres:Tous les livres
4
14152
772662
772355
2026-09-21T10:58:45Z
Xhungab
23827
Les modifications introduite 16 septembre 2026 à 13:50 par Lionel Scheepmans casse l' esthétique de la page, ne fait pas parti de la prise de décision. Déplacement des catégories en page d'accueil suite à la prise de décision.
772662
wikitext
text/x-wiki
<big>Cette page présente [[:Catégorie:Livres par titre|tous les livres]] disponibles sur Wikilivres, y compris les [[:Catégorie:Ébauches sans ressources suggérées|ébauches]], les [[:Catégorie:Livres en cours de rédaction|livres en cours de rédaction]], et les [[:Catégorie:Feuilles volantes|feuilles volantes]].</big>
N'hésitez pas à poursuivre l'écriture ou à améliorer les livres ou les pages en cours de création. Un Wiki est fait pour cela. Si vous le désirez, vous pouvez aussi contacter les personnes qui ont déjà contribué sur les pages que vous voulez modifier. Cela peut se faire en cliquant sur l'onglet « Voir l'historique » des pages en question.
[[Fichier:Pedia-shelf-1.jpg|right|250px]]
__NOEDITSECTION__
La bibliothèque contient [[Spécial:Statistics|{{NUMBEROFARTICLES}}]] pages réparties en [[:Catégorie:Livres par titre|{{formatnum:{{NUMBEROFBOOKS}}}} livres]], dont [[:Catégorie:Livres avec version PDF|{{PAGESINCATEGORY:Livres avec version PDF}}]] téléchargeables en PDF.
{{message|clear=left|style=margin:0.2em 0;|Il existe, pour le moment, deux systèmes d'indexation internes pour trouver du contenu :
* Le [[#Tous les livres par catégorie|système de catégories]] {{100}}. La quasi-intégralité des livres peuvent être trouvés via ce système.
* La [[#Tous les livres selon la classification décimale universelle|classification décimale universelle]] {{25}}, utilisée dans les bibliothèques. Hélas, beaucoup de livres ne sont pas encore rangés selon cette classification, préférez pour le moment la première méthode ou la [[Wikilivres:Rechercher un livre|recherche de livre]].
}}
<div class="flex-content">
<div class="flex-content-half">
<div class="headerbleu">Livres par thème</div>
<categorytree hideroot="on" mode="pages" style="border:1px solid #A0A0A0; padding:0.7ex; padding-right:1em;">Livres par thème</categorytree>
</div>
<div class="flex-content-half">
<div class="headerbleu">Livres par niveau d'avancement</div>
<categorytree hideroot="on" mode="pages" style="border:1px solid #A0A0A0; padding:0.7ex;">Livres par niveau d'avancement</categorytree>
</div>
</div>
== Tous les livres selon la classification décimale universelle ==
{{:Wikilivres:CDU}}
[[Catégorie:Wikilivres]]
cngz5hzotrv6vw1ranuggd2vt5af58w
Photographie/Personnalités/R/Andreas Reiser
0
50067
772651
373874
2026-09-21T05:15:14Z
Ziv
119944
([[c:GR|GR]]) [[c:COM:Duplicate|Duplicate]]: [[File:Reiser - Jeune femme fellah.jpg]] → [[File:067- Anonym, c. 1880.jpg]] Exact or scaled-down duplicate: [[c::File:067- Anonym, c. 1880.jpg]]
772651
wikitext
text/x-wiki
{{Ph s Personnalités}}
'''Andreas Reiser''' était un photographe allemand, travaillant en Égypte, et auteur de nombreuses [[cartes postales]] orientalistes.
== Galerie de photographies ==
<gallery widths="240px" heights="240px">
File:Reiser - Jeune fille fellah.jpg
File:067- Anonym, c. 1880.jpg
File:Reiser - Jeune barbarine.jpg
File:Reiser - 138 001.jpg
File:Reiser - La danse.jpg
File:Jeune fille egyptienne seins nus- REISER - SIP .jpg
File:REISER, Jeune indigène (SIP).jpg
File:REISER, Femme égyptienne (SIP).jpg
File:REISER, Femme indigène (LH).jpg
</gallery>
hj4mpxedyhv3vrfvkoxglmpvaff7ed7
Fonctionnement d'un ordinateur/Les processeurs superscalaires
0
65956
772495
772477
2026-09-20T17:00:12Z
Mewtow
31375
/* L'impact de la superscalarité sur le front-end */
772495
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 multi-cycles 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'''.
==Les différents types de CPU à émission multiple==
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 superscalaires et VLIW===
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.
===Les processeurs superscalaires étroits et larges===
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, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le 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]]
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliqué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, ce qui permet d'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 mémoire en parallèle d'autres micro-opération entières/flottantes. 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.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
nvmf2zyg5xd3kjxaugtyufu18xgdks3
772496
772495
2026-09-20T17:06:27Z
Mewtow
31375
/* L'impact de la superscalarité sur le chemin de données */
772496
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 multi-cycles 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'''.
==Les différents types de CPU à émission multiple==
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 superscalaires et VLIW===
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.
===Les processeurs superscalaires étroits et larges===
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, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le 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]]
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliqué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, ce qui permet d'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 mémoire en parallèle d'autres micro-opération entières/flottantes. 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.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ka7wbrknruenhfqxiq26fqtht8mhmvo
772497
772496
2026-09-20T17:20:42Z
Mewtow
31375
772497
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 multi-cycles 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.
==Les différents types de CPU superscalaires==
Il est possible de classer les CPU superscalaires en plusieurs sous-types, bien que ce soit assez peu courant. La majorité de ces sous-types se distinguent quant au nombre de µops qu'ils peuvent exécuter en même temps.
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, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
===Les processeurs superscalaires étroits et larges===
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le 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]]
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliqué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, ce qui permet d'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 mémoire en parallèle d'autres micro-opération entières/flottantes. 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.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
iltmg2b8eb5gdqasijom80sk4tyoqf2
772498
772497
2026-09-20T17:50:49Z
Mewtow
31375
/* Les différents types de CPU superscalaires */
772498
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 multi-cycles 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.
==Généralités sur les CPU superscalaires==
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, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
===Les processeurs superscalaires étroits et larges===
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le 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]]
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliqué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, ce qui permet d'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 mémoire en parallèle d'autres micro-opération entières/flottantes. 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.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
q3pv6weewnlai0r8bgszt7mq2dnnpzn
772499
772498
2026-09-20T18:48:39Z
Mewtow
31375
/* Les unités de calcul des processeurs superscalaires */
772499
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 multi-cycles 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.
==Généralités sur les CPU superscalaires==
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, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
===Les processeurs superscalaires étroits et larges===
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et de modifier l'unité d'émission. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. L cout en transistors était donc minimal, pour un gain en performance certes assez limité.
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
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]]
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
Intuitivement, on se dit qu'il est possible de faire pareil qu'avec la FPU, mais pour l'unité mémoire. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'une micro-opération entières et d'une micro-opération flottante. On parle alors de '''triple émission entière-flottante-mémoire'''. Cependant, ce n'est pas la solution qui a été utilisée. Elle a en effet trop de contraintes.
Sans cette optimisation, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Avec ce genre de triple émission, il aurait fallu incorporer une unité de calcul d'adresse dédiée, si elle n'existait pas déjà. De plus, il faut rajouter plusieurs ports sur le banc de registre : un port de lecture pour lire les données à écrire, un port d'écriture pour écrire la donnée lue.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ttepn264v4rly6vejmle660sre84en8
772500
772499
2026-09-20T18:48:52Z
Mewtow
31375
/* La double émission entière-flottante */
772500
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 multi-cycles 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.
==Généralités sur les CPU superscalaires==
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, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
===Les processeurs superscalaires étroits et larges===
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et de modifier l'unité d'émission. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. L cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
Intuitivement, on se dit qu'il est possible de faire pareil qu'avec la FPU, mais pour l'unité mémoire. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'une micro-opération entières et d'une micro-opération flottante. On parle alors de '''triple émission entière-flottante-mémoire'''. Cependant, ce n'est pas la solution qui a été utilisée. Elle a en effet trop de contraintes.
Sans cette optimisation, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Avec ce genre de triple émission, il aurait fallu incorporer une unité de calcul d'adresse dédiée, si elle n'existait pas déjà. De plus, il faut rajouter plusieurs ports sur le banc de registre : un port de lecture pour lire les données à écrire, un port d'écriture pour écrire la donnée lue.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ovqd7u94kl88vre5f0nw9gx03zpy8ez
772501
772500
2026-09-20T18:51:02Z
Mewtow
31375
/* La triple émission entière-flottante-branchements */
772501
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 multi-cycles 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.
==Généralités sur les CPU superscalaires==
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, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
===Les processeurs superscalaires étroits et larges===
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et de modifier l'unité d'émission. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. L cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
Intuitivement, on se dit qu'il est possible de faire pareil qu'avec la FPU, mais pour l'unité mémoire. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'une micro-opération entières et d'une micro-opération flottante. On parle alors de '''triple émission entière-flottante-mémoire'''. La raison est que l'on peut utiliser une amélioration assez simple de cette triple émission, qui a un cout en circuit minimal, mais qui donne un gain en performance conséquent.
Sans cette optimisation, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Avec ce genre de triple émission, il aurait fallu incorporer une unité de calcul d'adresse dédiée, si elle n'existait pas déjà. De plus, il faut rajouter plusieurs ports sur le banc de registre : un port de lecture pour lire les données à écrire, un port d'écriture pour écrire la donnée lue.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
dhh4ahq1exai1fk5mq48qnertzdoop6
772502
772501
2026-09-20T19:12:59Z
Mewtow
31375
/* La triple émission entière-flottante-branchements */
772502
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 multi-cycles 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.
==Généralités sur les CPU superscalaires==
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, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
===Les processeurs superscalaires étroits et larges===
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et de modifier l'unité d'émission. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. L cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que cela empêche d'utiliser l'ALU entière pour faire les calculs d'adresse. Avec ce genre de triple émission, il aurait fallu incorporer une unité de calcul d'adresse dédiée, si elle n'existait pas déjà. De plus, il faut rajouter des ports sur le banc de registre, avec un port de lecture pour la donnée à écrire, un port d'écriture pour la donnée lue.
A la place, les CPU historiques ont pris un chemin différents. Ils avaient une forme de triple émission différente, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
9a1qjpwrvaak59q6spprukb1wwpr7wn
772503
772502
2026-09-20T19:19:46Z
Mewtow
31375
/* La double émission entière-flottante */
772503
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 multi-cycles 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.
==Généralités sur les CPU superscalaires==
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, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
===Les processeurs superscalaires étroits et larges===
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et de modifier l'unité d'émission en ajoutant un port d'émission dédié sur la FPU. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que cela empêche d'utiliser l'ALU entière pour faire les calculs d'adresse. Avec ce genre de triple émission, il aurait fallu incorporer une unité de calcul d'adresse dédiée, si elle n'existait pas déjà. De plus, il faut rajouter des ports sur le banc de registre, avec un port de lecture pour la donnée à écrire, un port d'écriture pour la donnée lue.
A la place, les CPU historiques ont pris un chemin différents. Ils avaient une forme de triple émission différente, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
j0t1xnenvhe7z8ushsyas5wu3k1fxvm
772504
772503
2026-09-20T19:22:08Z
Mewtow
31375
/* La triple émission entière-flottante-branchements */
772504
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 multi-cycles 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.
==Généralités sur les CPU superscalaires==
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, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
===Les processeurs superscalaires étroits et larges===
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et de modifier l'unité d'émission en ajoutant un port d'émission dédié sur la FPU. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
gm5u7iprxduzv3exxzs1cbbc7me6kgt
772505
772504
2026-09-20T19:34:22Z
Mewtow
31375
/* L'implémentation d'un processeur superscalaire */
772505
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 multi-cycles 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.
==Généralités sur les CPU superscalaires==
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, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
===Les processeurs superscalaires étroits et larges===
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et de modifier l'unité d'émission en ajoutant un port d'émission dédié sur la FPU. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
slum9ak5gktxphfu5y2r8j8jzuvw7gp
772506
772505
2026-09-20T19:37:24Z
Mewtow
31375
/* Généralités sur les CPU superscalaires */
772506
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 multi-cycles 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.
==Les processeurs superscalaires étroits et larges==
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, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et de modifier l'unité d'émission en ajoutant un port d'émission dédié sur la FPU. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
aocam2qo4vaxwuz61ns3zqusfqjd79l
772507
772506
2026-09-20T19:58:18Z
Mewtow
31375
772507
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 multi-cycles 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.
==Les processeurs superscalaires étroits et larges==
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, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. Et les tout premiers processeurs superscalaires étaient très différents des CPU superscalaires modernes, ce qui fait qu'il vaut mieux les aborder à part.
==Les processeurs superscalaires historiques==
Les tout premiers processeurs superscalaires étaient très limités. Ils avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission avait des paires d'instructions autorisées et des paires interdites.. Par exemple, il ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Et notamment, il n'y avait pas besoin de dupliquer la moindre ALU ! Au contraire, les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et de modifier l'unité d'émission en ajoutant un port d'émission dédié sur la FPU. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ejhuf0v47o6mbx8umbhevvknyriuzlp
772508
772507
2026-09-20T19:59:35Z
Mewtow
31375
/* Les processeurs superscalaires étroits et larges */
772508
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 multi-cycles 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.
==Les processeurs superscalaires étroits et larges==
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 triple émission permettent eux d’émettre trois instructions à la fois. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. Et les tout premiers processeurs superscalaires étaient très différents des CPU superscalaires modernes, ce qui fait qu'il vaut mieux les aborder à part.
==Les processeurs superscalaires historiques==
Les tout premiers processeurs superscalaires étaient très limités. Ils avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission avait des paires d'instructions autorisées et des paires interdites.. Par exemple, il ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Et notamment, il n'y avait pas besoin de dupliquer la moindre ALU ! Au contraire, les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et de modifier l'unité d'émission en ajoutant un port d'émission dédié sur la FPU. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
7zet6ihsge87qb3xx9i0qomkaaxjj25
772509
772508
2026-09-20T20:00:31Z
Mewtow
31375
/* Les processeurs superscalaires historiques */
772509
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 multi-cycles 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.
==Les processeurs superscalaires étroits et larges==
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 triple émission permettent eux d’émettre trois instructions à la fois. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. Et les tout premiers processeurs superscalaires étaient très différents des CPU superscalaires modernes, ce qui fait qu'il vaut mieux les aborder à part.
==Les processeurs superscalaires historiques==
Les tout premiers processeurs superscalaires étaient très limités. Ils avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Et notamment, il n'y avait pas besoin de dupliquer la moindre ALU ! Au contraire, les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et de modifier l'unité d'émission en ajoutant un port d'émission dédié sur la FPU. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
c4jwkftfdm4or1yv8qvhg8dcslf1bxo
772510
772509
2026-09-20T20:01:55Z
Mewtow
31375
/* La double émission entière-flottante */
772510
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 multi-cycles 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.
==Les processeurs superscalaires étroits et larges==
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 triple émission permettent eux d’émettre trois instructions à la fois. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. Et les tout premiers processeurs superscalaires étaient très différents des CPU superscalaires modernes, ce qui fait qu'il vaut mieux les aborder à part.
==Les processeurs superscalaires historiques==
Les tout premiers processeurs superscalaires étaient très limités. Ils avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Et notamment, il n'y avait pas besoin de dupliquer la moindre ALU ! Au contraire, les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
irryrtxhqqxbn0ugl6fonxoh0yin2fb
772511
772510
2026-09-20T20:32:17Z
Mewtow
31375
772511
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 multi-cycles 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.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Et notamment, il n'y avait pas besoin de dupliquer la moindre ALU ! Au contraire, les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===Les processeurs superscalaires étroits et larges===
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. Et les tout premiers processeurs superscalaires étaient très différents des CPU superscalaires modernes, ce qui fait qu'il vaut mieux les aborder à part.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
h2e7q504f8qx71swvcpwl32kap34sno
772512
772511
2026-09-20T20:36:12Z
Mewtow
31375
/* Les processeurs superscalaires historiques */
772512
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 multi-cycles 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.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===Les processeurs superscalaires étroits et larges===
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. Et les tout premiers processeurs superscalaires étaient très différents des CPU superscalaires modernes, ce qui fait qu'il vaut mieux les aborder à part.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
328c0ruodujglhkhrfcxsmsg5q4stqb
772513
772512
2026-09-20T20:42:10Z
Mewtow
31375
/* Les processeur superscalaire modernes (non-historiques) */
772513
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 multi-cycles 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.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===Les processeurs superscalaires étroits et larges===
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. Et les tout premiers processeurs superscalaires étaient très différents des CPU superscalaires modernes, ce qui fait qu'il vaut mieux les aborder à part.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
9g0r1w7ez8n9vfmpfsh4zulxd7g70s3
772514
772513
2026-09-20T20:57:46Z
Mewtow
31375
/* Les processeur superscalaire modernes (non-historiques) */
772514
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 multi-cycles 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.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
==L'implémentation des processeurs superscalaires==
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. Et les tout premiers processeurs superscalaires étaient très différents des CPU superscalaires modernes, ce qui fait qu'il vaut mieux les aborder à part.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
*L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
olgqhlyimgfsbhpnts6f2w5olm7qy9t
772515
772514
2026-09-20T20:58:10Z
Mewtow
31375
772515
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 multi-cycles 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 des processeurs superscalaires==
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. Et les tout premiers processeurs superscalaires étaient très différents des CPU superscalaires modernes, ce qui fait qu'il vaut mieux les aborder à part.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
*L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ts7uip38dt60l73j6jolblnzhjtdgv3
772516
772515
2026-09-20T20:58:31Z
Mewtow
31375
/* L'implémentation des processeurs superscalaires */
772516
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. Et les tout premiers processeurs superscalaires étaient très différents des CPU superscalaires modernes, ce qui fait qu'il vaut mieux les aborder à part.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
*L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
3239zc02aql2p6yqiumbs9pui4p40ie
772517
772516
2026-09-20T21:02:46Z
Mewtow
31375
/* L'implémentation des processeurs superscalaires */
772517
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Les explications précédentes peuvent faire croire que les unités de calcul sont dupliquées, mais la réalité est parfois différente. Les unités de calcul ne sont pas toutes dupliquées, et il existe des processeurs sur lesquels aucune ne l'est. Diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances. Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
2yacwid3bsbom9mie1ckhitel6uwn9y
772518
772517
2026-09-20T21:03:07Z
Mewtow
31375
/* Les processeur superscalaire modernes (non-historiques) */
772518
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Les explications précédentes peuvent faire croire que les unités de calcul sont dupliquées, mais la réalité est parfois différente. Les unités de calcul ne sont pas toutes dupliquées, et il existe des processeurs sur lesquels aucune ne l'est. Diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances. Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
p5l2avs5e3c7jjor43vat2jm8hwil7y
772519
772518
2026-09-20T21:05:36Z
Mewtow
31375
/* L’implémentation réelle n'est pas forcément celle décrite plus haut */
772519
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, on va considérer qu'elles sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALUs ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité.
Pour le banc de registre, il n'est pas dupliqué, mais on lui rajoute des ports de lecture et d'écriture. Qui dit plus d'instructions exécutes en même temps dit plus d'opérandes à lire et de résultats à enregistres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
b407rkzzz2rd2132asukubtvmce021y
772520
772519
2026-09-20T21:11:42Z
Mewtow
31375
/* L'impact de la superscalarité sur le chemin de données */
772520
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ?
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|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 les ALU entières. 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'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
0alsjum2efua9kjclbss2fss4868qg9
772521
772520
2026-09-20T21:12:16Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772521
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ?
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
b67m7yhk47zmhrvirflcj2q6j9wpdh6
772522
772521
2026-09-20T21:23:47Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772522
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
[[File:Emission multiple des opérations entières, implémentation naive.png|thumb|Emission multiple des opérations entières, implémentation naive]]
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Lesz deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication. Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle.
Pour les branchements, deux solutions sont possibles. La première utilise une unité de branchements séparée, avec son propre port d'émission. Le processeur de l'exemple passe alors de la triple à la quadruple émission, car il peut émettre deux opérations entières, un branchement et une opération flottante. Une autre solution est que les branchements sont traités comme des opérations entières de base. C'est le cas si les branchements sont pris en charge dans l'unité de calcul entière. Ou encore, il est possible de relier l'unité de branchement à un port d'émission partagé avec une ALU entière.
[[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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
7crkimwt09v5xs28i7tubl18cif1dj7
772523
772522
2026-09-20T21:27:08Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772523
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
[[File:Emission multiple des opérations entières, implémentation naive.png|thumb|Emission multiple des opérations entières, implémentation naive]]
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Lesz deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle.
[[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.]]
Pour les branchements, deux solutions sont possibles. La première utilise une unité de branchements séparée, avec son propre port d'émission. Le processeur de l'exemple passe alors de la triple à la quadruple émission, car il peut émettre deux opérations entières, un branchement et une opération flottante. Une autre solution est que les branchements sont traités comme des opérations entières de base. C'est le cas si les branchements sont pris en charge dans l'unité de calcul entière. Ou encore, il est possible de relier l'unité de branchement à un port d'émission partagé avec une ALU entière.
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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
8ocksb16vyajf644ws9gvj7uth3df0n
772524
772523
2026-09-20T21:27:26Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772524
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
[[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.]]
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Lesz deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle.
Pour les branchements, deux solutions sont possibles. La première utilise une unité de branchements séparée, avec son propre port d'émission. Le processeur de l'exemple passe alors de la triple à la quadruple émission, car il peut émettre deux opérations entières, un branchement et une opération flottante. Une autre solution est que les branchements sont traités comme des opérations entières de base. C'est le cas si les branchements sont pris en charge dans l'unité de calcul entière. Ou encore, il est possible de relier l'unité de branchement à un port d'émission partagé avec une ALU entière.
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, ce qui fait que les ports d'émission seront sous-utilisés. 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.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|É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=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
bzvwxor40mabmzlzzxz8oi7fcvgt2ru
772525
772524
2026-09-20T21:30:04Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772525
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
[[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.]]
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
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=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. La première utilise une unité de branchements séparée, avec son propre port d'émission. Le processeur de l'exemple passe alors de la triple à la quadruple émission, car il peut émettre deux opérations entières, un branchement et une opération flottante. Une autre solution est que les branchements sont traités comme des opérations entières de base. C'est le cas si les branchements sont pris en charge dans l'unité de calcul entière. Ou encore, il est possible de relier l'unité de branchement à un port d'émission partagé avec une ALU entière.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
m0va5xeslhtpjqgxu25soeyru0ayobj
772526
772525
2026-09-20T21:32:02Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772526
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
[[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.]]
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. La première utilise une unité de branchements séparée, avec son propre port d'émission. Le processeur de l'exemple passe alors de la triple à la quadruple émission, car il peut émettre deux opérations entières, un branchement et une opération flottante. Une autre solution est que les branchements sont traités comme des opérations entières de base. C'est le cas si les branchements sont pris en charge dans l'unité de calcul entière. Ou encore, il est possible de relier l'unité de branchement à un port d'émission partagé avec une ALU entière.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
seshisx6aztbvyix8jcbrc5gp66rqlf
772527
772526
2026-09-20T21:32:26Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772527
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. La première utilise une unité de branchements séparée, avec son propre port d'émission. Le processeur de l'exemple passe alors de la triple à la quadruple émission, car il peut émettre deux opérations entières, un branchement et une opération flottante. Une autre solution est que les branchements sont traités comme des opérations entières de base. C'est le cas si les branchements sont pris en charge dans l'unité de calcul entière. Ou encore, il est possible de relier l'unité de branchement à un port d'émission partagé avec une ALU entière.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ihsp5xygkhlrfdf0lx656dp33h05sdu
772528
772527
2026-09-20T21:36:52Z
Mewtow
31375
/* Les processeur superscalaire modernes (non-historiques) */
772528
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. La première utilise une unité de branchements séparée, avec son propre port d'émission. Le processeur de l'exemple passe alors de la triple à la quadruple émission, car il peut émettre deux opérations entières, un branchement et une opération flottante. Une autre solution est que les branchements sont traités comme des opérations entières de base. C'est le cas si les branchements sont pris en charge dans l'unité de calcul entière. Ou encore, il est possible de relier l'unité de branchement à un port d'émission partagé avec une ALU entière.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
h3r2xhxtg9o3tla4ylxfw26f2tjvnuw
772529
772528
2026-09-20T21:40:44Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772529
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Les accès mémoire sont un détail d'implémentation très important. Une instruction LOAD/STORE est traitée comme une opération entière spéciale, sur ces processeurs. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. La raison est qu'une unité de calcul est utilisée pour faire les calculs d'adresse. L'unité mémoire est donc à la suite d'une des unités de calcul.
Pour les branchements, deux solutions sont possibles. Soit les branchements sont traités comme des instructions entières, soit il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Avec la première solution, les branchements sont exécutés dans une ALU entière, ou dans une unité de branchement séparée. Avec la seconde, pas le choix, il faut une unité de branchement séparée, avec un port d'émission dédié.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
bvx93vmyci0mbh5k1tvpjen0jfb5o75
772530
772529
2026-09-20T21:47:11Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772530
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais la ''triple émission entière-flottante-mémoire'' n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Les accès mémoire sont un détail d'implémentation très important. Une instruction LOAD/STORE est traitée comme une opération entière spéciale, sur ces processeurs. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. La raison est qu'une unité de calcul est utilisée pour faire les calculs d'adresse. L'unité mémoire est donc à la suite d'une des unités de calcul.
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
d8qs0kvckdj662luv0nkp8fvs2l1ocv
772531
772530
2026-09-20T21:48:38Z
Mewtow
31375
/* La triple émission entière-flottante-branchements */
772531
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais cette solution n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants. L'idée était de pouvoir émettre un branchement en parallèle d'une instruction entière. Le processeur pouvait donc émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. La techniques s'appelle l''''émission parallèle des branchements'''.
Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Les accès mémoire sont un détail d'implémentation très important. Une instruction LOAD/STORE est traitée comme une opération entière spéciale, sur ces processeurs. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. La raison est qu'une unité de calcul est utilisée pour faire les calculs d'adresse. L'unité mémoire est donc à la suite d'une des unités de calcul.
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
6vq3vavba3fvxw13sw5wjwlbywhb6k0
772532
772531
2026-09-20T21:50:10Z
Mewtow
31375
/* La triple émission entière-flottante-branchements */
772532
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===La triple émission entière-flottante-branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais cette solution n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Les accès mémoire sont un détail d'implémentation très important. Une instruction LOAD/STORE est traitée comme une opération entière spéciale, sur ces processeurs. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. La raison est qu'une unité de calcul est utilisée pour faire les calculs d'adresse. L'unité mémoire est donc à la suite d'une des unités de calcul.
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
9l0slwox7m3ra04nyvqwzbvb555n49h
772533
772532
2026-09-20T21:50:36Z
Mewtow
31375
/* La triple émission entière-flottante-branchements */
772533
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais cette solution n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Les accès mémoire sont un détail d'implémentation très important. Une instruction LOAD/STORE est traitée comme une opération entière spéciale, sur ces processeurs. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. La raison est qu'une unité de calcul est utilisée pour faire les calculs d'adresse. L'unité mémoire est donc à la suite d'une des unités de calcul.
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
n3k02t08hob9ireddzacz8d7hv69gvd
772534
772533
2026-09-20T21:50:52Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772534
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais cette solution n'a pas été utilisée, à ma connaissance. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple de micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple de micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Les accès mémoire sont un détail d'implémentation très important. Une instruction LOAD/STORE est traitée comme une opération entière spéciale, sur ces processeurs. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. La raison est qu'une unité de calcul est utilisée pour faire les calculs d'adresse. L'unité mémoire est donc à la suite d'une des unités de calcul.
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
2r2m3n8att77x8v41cngrnygbmldq0r
772535
772534
2026-09-20T21:51:40Z
Mewtow
31375
/* L'émission parallèle des branchements */
772535
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur.
Vous avez peut-être pensé à faire pareil qu'avec la FPU, mais pour l'unité mémoire. Mais cette solution n'a pas été utilisée immédiatement. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple de micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple de micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Les accès mémoire sont un détail d'implémentation très important. Une instruction LOAD/STORE est traitée comme une opération entière spéciale, sur ces processeurs. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. La raison est qu'une unité de calcul est utilisée pour faire les calculs d'adresse. L'unité mémoire est donc à la suite d'une des unités de calcul.
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
tg3avwhnouaou572crasxcgkew80zbn
772536
772535
2026-09-20T21:53:59Z
Mewtow
31375
/* L'émission parallèle des branchements */
772536
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple de micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple de micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Les accès mémoire sont un détail d'implémentation très important. Une instruction LOAD/STORE est traitée comme une opération entière spéciale, sur ces processeurs. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. La raison est qu'une unité de calcul est utilisée pour faire les calculs d'adresse. L'unité mémoire est donc à la suite d'une des unités de calcul.
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
7jw7gsja4pj7a6rn4qx2zz8kjixsz13
772537
772536
2026-09-20T21:54:20Z
Mewtow
31375
/* Les processeur superscalaire modernes (non-historiques) */
772537
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les processeurs superscalaires historiques==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple de micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple de micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Les accès mémoire sont un détail d'implémentation très important. Une instruction LOAD/STORE est traitée comme une opération entière spéciale, sur ces processeurs. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. La raison est qu'une unité de calcul est utilisée pour faire les calculs d'adresse. L'unité mémoire est donc à la suite d'une des unités de calcul.
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission paralléle des accès mémoire===
Il a fallu attendre pour que l'unité mémoire ait ses propres ports d'émission. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
6vs698kinc4gqa4g2qowsmdggc7aa4n
772538
772537
2026-09-20T21:54:53Z
Mewtow
31375
/* Les processeurs superscalaires historiques */
772538
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple de micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple de micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Les accès mémoire sont un détail d'implémentation très important. Une instruction LOAD/STORE est traitée comme une opération entière spéciale, sur ces processeurs. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. La raison est qu'une unité de calcul est utilisée pour faire les calculs d'adresse. L'unité mémoire est donc à la suite d'une des unités de calcul.
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission paralléle des accès mémoire===
Il a fallu attendre pour que l'unité mémoire ait ses propres ports d'émission. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ew32h6irbnclav26j4brtjv9axhhu49
772539
772538
2026-09-20T21:56:53Z
Mewtow
31375
/* Les unités de calcul des processeurs superscalaires */
772539
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple de micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple de micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Les accès mémoire sont un détail d'implémentation très important. Une instruction LOAD/STORE est traitée comme une opération entière spéciale, sur ces processeurs. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. La raison est qu'une unité de calcul est utilisée pour faire les calculs d'adresse. L'unité mémoire est donc à la suite d'une des unités de calcul.
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission paralléle des accès mémoire===
Il a fallu attendre pour que l'unité mémoire ait ses propres ports d'émission. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ein3ancxa7u0oztq78mkzz2g5bs0nl2
772540
772539
2026-09-20T21:58:15Z
Mewtow
31375
/* L'émission multiple de micro-opérations entières */
772540
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple de micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple de micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont un détail d'implémentation très important. Une instruction LOAD/STORE est traitée comme une opération entière spéciale, sur ces processeurs. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. La raison est qu'une unité de calcul est utilisée pour faire les calculs d'adresse. L'unité mémoire est donc à la suite d'une des unités de calcul.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission paralléle des accès mémoire===
Il a fallu attendre pour que l'unité mémoire ait ses propres ports d'émission. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
k1l1emvoo17j1rhpg09sirnn6zsg3bf
772541
772540
2026-09-20T22:05:27Z
Mewtow
31375
/* L'émission multiple de micro-opérations entières */
772541
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple de micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple de micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission paralléle des accès mémoire===
Il a fallu attendre pour que l'unité mémoire ait ses propres ports d'émission. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
gzuanmiu3qfuloo7w0xwzgjebddku6w
772542
772541
2026-09-20T22:06:08Z
Mewtow
31375
/* L'émission multiple de micro-opérations entières */
772542
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple de micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple de micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
==Les processeur superscalaire modernes (non-historiques)==
Nous venons de voir les processeurs superscalaires historiques, et notamment à quel point ils sont peu différents des CPU non-superscalaires. Les processeurs superscalaires non-historiques sont complétement différents. Ils sont capables d'émettre plusieurs instructions entières en même temps, parfois plusieurs opérations flottantes, ou mémoire. Et cela implique que certains circuits soient dupliqués. Des unités de calcul doivent être dupliqués, il faut rajouter des ports de lecture/écriture sur les bancs de registre, les décodeurs sont eux aussi dupliqués, etc.
===L'émission paralléle des accès mémoire===
Il a fallu attendre pour que l'unité mémoire ait ses propres ports d'émission. Une raison possible est que les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. A la place, les CPU historiques ont pris un chemin différent, au cout en transistors plus faible et permettant des gains en performances assez intéressants.
===L'émission multiple des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===Les optimisations===
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; 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. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. 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 toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
9vll156nh5idaf1p4rb0if6m7qjznnr
772543
772542
2026-09-20T22:14:09Z
Mewtow
31375
/* Les processeur superscalaire modernes (non-historiques) */
772543
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple de micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple de micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)====
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
===L'émission parallèle des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
nllrcfk5flpidafhxnj3xdfy862ak3s
772544
772543
2026-09-20T22:17:54Z
Mewtow
31375
/* L’impact de la superscalarité sur les autres circuits */
772544
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple de micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple de micro-opérations entières'''.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)====
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
===L'émission parallèle des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
hmjur6iq02pn5q1t0zxqj5mpbj0n0r7
772545
772544
2026-09-20T22:20:11Z
Mewtow
31375
/* L'émission multiple de micro-opérations entières */
772545
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. Dans le cas de la triple émission, le ''barrel shifter'' et le multiplieur sont connectés à des ports d'émission séparés. Cela permet ainsi de faire une addition, un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)====
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
===L'émission parallèle des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
7qt2no2vodn2guwah273deng9k2axrb
772546
772545
2026-09-20T22:27:35Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772546
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)====
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
===L'émission parallèle des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
5857pnqp82ldw1k2pxqmap51s0vd3hp
772547
772546
2026-09-20T22:27:43Z
Mewtow
31375
/* Les processeurs superscalaires ultérieurs (quadruple émission et plus)= */
772547
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
===L'émission parallèle des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
tt3priqy6vhf338btrycvt2y8j0647v
772548
772547
2026-09-20T22:31:35Z
Mewtow
31375
/* L'unité d'émission superscalaire */
772548
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
===L'émission parallèle des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
mv1w9xdex3zagkqyagq3qg1yp3v8c8u
772549
772548
2026-09-20T22:31:46Z
Mewtow
31375
/* L'impact de la superscalarité sur l'unité d'émission */
772549
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
L'unité d'émission est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de 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, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, 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.
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'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
===L'émission parallèle des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
eswxsv12wbtbta597x49as705eakgza
772551
772549
2026-09-20T22:38:49Z
Mewtow
31375
/* L'impact de la superscalarité sur l'unité d'émission */
772551
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
L'unité de renommage de registre est modifiée de la même manière. Elle doit 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.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
===L'émission parallèle des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
g7sv7t7577iqa0irxxx733wwnmkcr3s
772552
772551
2026-09-20T22:39:00Z
Mewtow
31375
/* L'impact de la superscalarité sur l'unité d'émission */
772552
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
===L'émission parallèle des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
fis2il239cof9k1noi72lku3xofhnbr
772553
772552
2026-09-20T22:39:52Z
Mewtow
31375
/* Les unités d'émission/renommage d'un processeur superscalaire */
772553
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission ou d'exécution dans le désordre sont modifiés, ils voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
===L'émission parallèle des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
pmy6i2cwniocqwnkgpqinaryiqtw1ec
772554
772553
2026-09-20T22:40:36Z
Mewtow
31375
/* L’impact de la superscalarité sur les autres circuits */
772554
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
===L'émission parallèle des accès mémoire===
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est fréquent d'avoir des accès mémoire simultanés. L'unité mémoire est alors dupliquée, et on doit rajouter des ports à la ''load/store queue''. Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
pttu2g82pyst1df20x7kknjy1mm77pu
772555
772554
2026-09-20T22:41:47Z
Mewtow
31375
/* L'émission parallèle des accès mémoire */
772555
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ilcgwl1px3sulpdueo63nnuw8ussbgl
772556
772555
2026-09-20T22:49:02Z
Mewtow
31375
/* L'émission multiple des accès mémoire */
772556
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''.
Il arrive qu'on ajoute des ports de lecture/écriture au cache, mais le CPU dépasse rarement deux ou trois ports. Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
h25q0tuvuk6y6p8mgg17zvmi67nu8gk
772557
772556
2026-09-20T22:52:34Z
Mewtow
31375
/* L'émission multiple des accès mémoire */
772557
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
swfsftg4yhlfmm0s29dqdyqy53gn4dt
772558
772557
2026-09-20T22:52:46Z
Mewtow
31375
/* L'émission multiple des accès mémoire */
772558
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
caatr14g4lhrefinfjj27qpv7xravqe
772559
772558
2026-09-20T22:54:07Z
Mewtow
31375
/* L'émission multiple des micro-opérations flottantes */
772559
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===L'émission multiple des micro-opérations flottantes===
Il est 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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
9pn1ngdk2sh1wd19x64klh0f8aabggx
772560
772559
2026-09-20T22:59:39Z
Mewtow
31375
/* Les processeurs superscalaires ultérieurs (quadruple émission et plus) */
772560
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière | || ALU entière | || ALU entière | || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Une autre possibilité était la '''double émission flottante'''.
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.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===L'émission multiple des micro-opérations flottantes===
Il est 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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
cef0ug6re2ys7nkyavowudh1jl1w6it
772561
772560
2026-09-20T22:59:55Z
Mewtow
31375
/* Les processeurs superscalaires ultérieurs (quadruple émission et plus) */
772561
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Une autre possibilité était la '''double émission flottante'''.
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.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===L'émission multiple des micro-opérations flottantes===
Il est 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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
2rea4sj36f6dubbpnp4m4ra3w8piipz
772562
772561
2026-09-20T23:03:02Z
Mewtow
31375
/* Les processeurs superscalaires ultérieurs (quadruple émission et plus) */
772562
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires ultérieurs (quadruple émission et plus)===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Une autre possibilité, utilisée sur les processeurs Alpha, était la '''double émission flottante'''. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===L'émission multiple des micro-opérations flottantes===
Il est 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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
bcc7i4gtkfdgc92yndlc8ofj7cq1sey
772563
772562
2026-09-20T23:03:19Z
Mewtow
31375
/* Les processeurs superscalaires ultérieurs (quadruple émission et plus) */
772563
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires étroits à quadruple émission et plus===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Une autre possibilité, utilisée sur les processeurs Alpha, était la '''double émission flottante'''. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===L'émission multiple des micro-opérations flottantes===
Il est 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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
sywrcgcyw2vo1pu506rktrbph4f7xdh
772564
772563
2026-09-20T23:03:31Z
Mewtow
31375
/* L'évolution dans le temps des processeurs superscalaires */
772564
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires étroits à quadruple émission et plus===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Une autre possibilité, utilisée sur les processeurs Alpha, était la '''double émission flottante'''. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===L'émission multiple des micro-opérations flottantes===
Il est 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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ctbz2hh1y2kfske8k9t2sha0w9ag65d
772565
772564
2026-09-20T23:12:51Z
Mewtow
31375
/* Les processeurs superscalaires étroits à quadruple émission et plus */
772565
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires étroits à quadruple émission et plus===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Une autre possibilité, utilisée sur les processeurs Alpha, était la '''double émission flottante'''. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
La technique ne permet pas de faire les calculs d'adresse dans lo'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. 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, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
===L'émission multiple des micro-opérations flottantes===
Il est 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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ecdlohzwb19di23k608tmdey64jlzt6
772566
772565
2026-09-20T23:18:52Z
Mewtow
31375
/* Les unités de calcul des processeurs superscalaires larges */
772566
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires étroits à quadruple émission et plus===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Une autre possibilité, utilisée sur les processeurs Alpha, était la '''double émission flottante'''. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
La technique ne permet pas de faire les calculs d'adresse dans lo'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
===Les unités de calcul des processeurs superscalaires larges===
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
g1pg8x7onnwtv3xch3a7hw9lkphkose
772567
772566
2026-09-20T23:19:05Z
Mewtow
31375
/* Les unités de calcul des processeurs superscalaires larges */
772567
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires étroits à quadruple émission et plus===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Une autre possibilité, utilisée sur les processeurs Alpha, était la '''double émission flottante'''. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
La technique ne permet pas de faire les calculs d'adresse dans lo'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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é.
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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
4fmb8zyi44i6vjsitvtqnamzprvttir
772568
772567
2026-09-20T23:19:59Z
Mewtow
31375
/* Les unités de calcul des processeurs superscalaires larges */
772568
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires étroits à quadruple émission et plus===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Une autre possibilité, utilisée sur les processeurs Alpha, était la '''double émission flottante'''. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
La technique ne permet pas de faire les calculs d'adresse dans lo'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
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é.
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.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
06nxk3tu9kwk2s6kza2v5p8l6celinz
772569
772568
2026-09-20T23:22:15Z
Mewtow
31375
/* Les processeurs superscalaires étroits à quadruple émission et plus */
772569
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution dans le temps des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires étroits à quadruple émission et plus===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
La technique ne permet pas de faire les calculs d'adresse dans lo'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
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é.
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.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
qjhtp8tkbwd7c570h0htgnx5tgi5wpz
772570
772569
2026-09-20T23:22:49Z
Mewtow
31375
/* L'évolution dans le temps des processeurs superscalaires étroits */
772570
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires étroits à quadruple émission et plus===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
La technique ne permet pas de faire les calculs d'adresse dans lo'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
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é.
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.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
1gngy0z56u1o2xc0qgh9eagoxh2umx2
772571
772570
2026-09-20T23:37:37Z
Mewtow
31375
/* Les processeurs superscalaires étroits à quadruple émission et plus */
772571
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires étroits à quadruple émission et plus===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
Combinée au techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
L'émission parallèle des accès mémoire est une sorte de point de bascule dans l'évolution des CPU superscalaire. Si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé les techniques précédentes, à savoir l'émission parallèle des branchements, la double émission entière-flottante, la double émission flottante. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 5 voies, à pentuple émission, et au-delà, pas pour de la quadruple émission.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
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é.
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.]]
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
pbrsxn8dnbkkkctzu69nlcacpcfwmz1
772572
772571
2026-09-20T23:46:44Z
Mewtow
31375
/* Les unités de calcul des processeurs superscalaires larges */
772572
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires étroits à quadruple émission et plus===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
Combinée au techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
L'émission parallèle des accès mémoire est une sorte de point de bascule dans l'évolution des CPU superscalaire. Si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé les techniques précédentes, à savoir l'émission parallèle des branchements, la double émission entière-flottante, la double émission flottante. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 5 voies, à pentuple émission, et au-delà, pas pour de la quadruple émission.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
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é.
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.]]
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
qpzh81xp7vrrb5280zodur1ayjq4kch
772573
772572
2026-09-20T23:48:24Z
Mewtow
31375
/* L'émission entière/flottante sur les CPU superscalaires larges */
772573
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires étroits à quadruple émission et plus===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
Combinée au techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
L'émission parallèle des accès mémoire est une sorte de point de bascule dans l'évolution des CPU superscalaire. Si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé les techniques précédentes, à savoir l'émission parallèle des branchements, la double émission entière-flottante, la double émission flottante. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 5 voies, à pentuple émission, et au-delà, pas pour de la quadruple émission.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
Une conséquence ce partage 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.]]
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
rssshhzgnvkjdidav64lpbm4suuxnxy
772574
772573
2026-09-20T23:49:04Z
Mewtow
31375
/* L'émission entière/flottante sur les CPU superscalaires larges */
772574
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires étroits à quadruple émission et plus===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
Combinée au techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
L'émission parallèle des accès mémoire est une sorte de point de bascule dans l'évolution des CPU superscalaire. Si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé les techniques précédentes, à savoir l'émission parallèle des branchements, la double émission entière-flottante, la double émission flottante. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 5 voies, à pentuple émission, et au-delà, pas pour de la quadruple émission.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
===Le partage des ports d'émission : la ''port contention''===
Placer plusieurs unités de calcul sur un même port 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.]]
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
jqjdvlde4hamrdey2ccs57r4ovvhiry
772575
772574
2026-09-20T23:51:54Z
Mewtow
31375
772575
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise. Et ces nombres ne sortent pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires étroits à quadruple émission et plus===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
Combinée au techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
L'émission parallèle des accès mémoire est une sorte de point de bascule dans l'évolution des CPU superscalaire. Si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé les techniques précédentes, à savoir l'émission parallèle des branchements, la double émission entière-flottante, la double émission flottante. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 5 voies, à pentuple émission, et au-delà, pas pour de la quadruple émission.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
===Le partage des ports d'émission : la ''port contention''===
Placer plusieurs unités de calcul sur un même port 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.]]
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
foxqqu0v0m3eekav1ui6pbklqobfqdi
772576
772575
2026-09-20T23:52:04Z
Mewtow
31375
772576
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les processeurs superscalaires étroits à quadruple émission et plus===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
Combinée au techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
L'émission parallèle des accès mémoire est une sorte de point de bascule dans l'évolution des CPU superscalaire. Si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé les techniques précédentes, à savoir l'émission parallèle des branchements, la double émission entière-flottante, la double émission flottante. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 5 voies, à pentuple émission, et au-delà, pas pour de la quadruple émission.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
===Le partage des ports d'émission : la ''port contention''===
Placer plusieurs unités de calcul sur un même port 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.]]
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
62gtikbi5xwy07rmiotpcas1o4cvxu8
772577
772576
2026-09-20T23:53:22Z
Mewtow
31375
/* Les unités de calcul des processeurs superscalaires étroits */
772577
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
Combinée au techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
L'émission parallèle des accès mémoire est une sorte de point de bascule dans l'évolution des CPU superscalaire. Si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé les techniques précédentes, à savoir l'émission parallèle des branchements, la double émission entière-flottante, la double émission flottante. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 5 voies, à pentuple émission, et au-delà, pas pour de la quadruple émission.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
===Le partage des ports d'émission : la ''port contention''===
Placer plusieurs unités de calcul sur un même port 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.]]
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
1hqer6ukj27tumq247gj60czm1h7za0
772578
772577
2026-09-20T23:54:42Z
Mewtow
31375
772578
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
Combinée au techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
L'émission parallèle des accès mémoire est une sorte de point de bascule dans l'évolution des CPU superscalaire. Si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé les techniques précédentes, à savoir l'émission parallèle des branchements, la double émission entière-flottante, la double émission flottante. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 5 voies, à pentuple émission, et au-delà, pas pour de la quadruple émission.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
===Le partage des ports d'émission : la ''port contention''===
Placer plusieurs unités de calcul sur un même port 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.]]
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
q59zyolgkrf94pceeh8ou73bjshonp6
772579
772578
2026-09-20T23:57:47Z
Mewtow
31375
/* Le front-end d'un processeur superscalaire large */
772579
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
Combinée au techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
L'émission parallèle des accès mémoire est une sorte de point de bascule dans l'évolution des CPU superscalaire. Si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé les techniques précédentes, à savoir l'émission parallèle des branchements, la double émission entière-flottante, la double émission flottante. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 5 voies, à pentuple émission, et au-delà, pas pour de la quadruple émission.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
===Le partage des ports d'émission : la ''port contention''===
Placer plusieurs unités de calcul sur un même port 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.]]
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
eyvwn56pvzwfjkmy5kjuhnsxbq7acue
772580
772579
2026-09-21T00:03:31Z
Mewtow
31375
/* Le front-end d'un processeur superscalaire large */
772580
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
Combinée au techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
L'émission parallèle des accès mémoire est une sorte de point de bascule dans l'évolution des CPU superscalaire. Si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé les techniques précédentes, à savoir l'émission parallèle des branchements, la double émission entière-flottante, la double émission flottante. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 5 voies, à pentuple émission, et au-delà, pas pour de la quadruple émission.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
===Le partage des ports d'émission : la ''port contention''===
Placer plusieurs unités de calcul sur un même port 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.]]
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
apj192tz81tju4i72lfury4qivqr1m9
772581
772580
2026-09-21T00:07:06Z
Mewtow
31375
/* Les CPU superscalaires de la "zone grise" */
772581
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
Combinée au techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
L'émission parallèle des accès mémoire est une sorte de point de bascule dans l'évolution des CPU superscalaire. Si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé les techniques précédentes, à savoir l'émission parallèle des branchements, la double émission entière-flottante, la double émission flottante. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
===Le partage des ports d'émission : la ''port contention''===
Placer plusieurs unités de calcul sur un même port 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.]]
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
rmvfwjiqdg9n2zxrafigpy1qnpalf3g
772582
772581
2026-09-21T00:10:31Z
Mewtow
31375
/* Les unités de calcul des processeurs superscalaires larges */
772582
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
Combinée au techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
L'émission parallèle des accès mémoire est une sorte de point de bascule dans l'évolution des CPU superscalaire. Si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé les techniques précédentes, à savoir l'émission parallèle des branchements, la double émission entière-flottante, la double émission flottante. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
5idbhs6vcuiemrlpbtoupncs3pc4uhq
772583
772582
2026-09-21T00:10:42Z
Mewtow
31375
/* Les unités de calcul des processeurs superscalaires étroits */
772583
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
Combinée au techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
L'émission parallèle des accès mémoire est une sorte de point de bascule dans l'évolution des CPU superscalaire. Si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé les techniques précédentes, à savoir l'émission parallèle des branchements, la double émission entière-flottante, la double émission flottante. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
===Le partage des ports d'émission : la ''port contention''===
Placer plusieurs unités de calcul sur un même port 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.]]
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
61hdijg2u5xc30y6w3ly32hnwjpiorh
772584
772583
2026-09-21T00:20:52Z
Mewtow
31375
/* Les CPU superscalaires de la "zone grise" */
772584
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
==Les CPU superscalaires de la "zone grise"==
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
===Les CPU superscalaires intermédiaires inspirés des design à triple émission===
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
===Les CPU superscalaires intermédiaires avec nouvelles techniques===
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
===Résumé===
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
===Le partage des ports d'émission : la ''port contention''===
Placer plusieurs unités de calcul sur un même port 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.]]
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
jct36vmkytl2bkudt62cc0188v4xpp2
772585
772584
2026-09-21T00:21:15Z
Mewtow
31375
/* Les CPU superscalaires intermédiaires avec nouvelles techniques */
772585
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
==Les CPU superscalaires de la "zone grise"==
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
===Les CPU superscalaires intermédiaires inspirés des design à triple émission===
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
===Les CPU superscalaires intermédiaires avec émission parallèle===
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
===Résumé===
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
===Le partage des ports d'émission : la ''port contention''===
Placer plusieurs unités de calcul sur un même port 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.]]
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===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 ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
6m784k4xknbu7izerij9qz0llx0riw0
772586
772585
2026-09-21T00:23:04Z
Mewtow
31375
/* Résumé */
772586
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
==Les CPU superscalaires de la "zone grise"==
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
===Les CPU superscalaires intermédiaires inspirés des design à triple émission===
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
===Les CPU superscalaires intermédiaires avec émission parallèle===
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
===Résumé===
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
===Le partage des ports d'émission : la ''port contention''===
Placer plusieurs unités de calcul sur un même port 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.]]
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ghe1wcnfmaghq8pf50nljj0ea7lq63y
772587
772586
2026-09-21T00:23:21Z
Mewtow
31375
/* Le partage des ports d'émission : la port contention */
772587
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
==Les CPU superscalaires de la "zone grise"==
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
===Les CPU superscalaires intermédiaires inspirés des design à triple émission===
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
===Les CPU superscalaires intermédiaires avec émission parallèle===
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
===Résumé===
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
===Le partage des ports d'émission : la ''port contention''===
Placer plusieurs unités de calcul sur un même port 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.]]
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.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
4lg5ai1pdceaqan2qdy01xvenrdrbo9
772588
772587
2026-09-21T00:23:41Z
Mewtow
31375
/* L'émission multiple des accès mémoire */
772588
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
==Les CPU superscalaires de la "zone grise"==
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
===Les CPU superscalaires intermédiaires inspirés des design à triple émission===
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
===Les CPU superscalaires intermédiaires avec émission parallèle===
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
===Résumé===
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
===L'émission entière/flottante sur les CPU superscalaires larges===
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
===Le partage des ports d'émission : la ''port contention''===
Placer plusieurs unités de calcul sur un même port 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.]]
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.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
l57sr4dmupu8xeeo2fe9lu8e8m45grg
772589
772588
2026-09-21T00:25:34Z
Mewtow
31375
/* Les unités de calcul des processeurs superscalaires larges */
772589
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
==Les CPU superscalaires de la "zone grise"==
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
===Les CPU superscalaires intermédiaires inspirés des design à triple émission===
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
===Les CPU superscalaires intermédiaires avec émission parallèle===
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
===Résumé===
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission : la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
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, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
sv4s5yoy2tvxjtj85kv3jqyegkqrjvf
772590
772589
2026-09-21T00:26:17Z
Mewtow
31375
/* Les CPU superscalaires de la "zone grise" */
772590
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission : la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
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, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
loorzhc1bwe07b709wv7ow5mwhcn285
772591
772590
2026-09-21T00:31:20Z
Mewtow
31375
/* Le partage des ports d'émission : la port contention */
772591
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
e3t6866ve63ncbts9u5suy4ixm2k3k1
772592
772591
2026-09-21T00:33:01Z
Mewtow
31375
/* L'exécution dans le désordre sur un processeur superscalaire */
772592
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
8c6uae7zchrlgmm0rpfcvsqz9crxf3m
772593
772592
2026-09-21T00:34:05Z
Mewtow
31375
/* L'exécution dans le désordre sur un processeur superscalaire */
772593
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
d2s0zuzcbtwsws99b0uxyfrdnal1gb1
772594
772593
2026-09-21T00:34:36Z
Mewtow
31375
/* L'unité d'émission superscalaire */
772594
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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. Si le renommage de registre est utilisé, 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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
r9jfcbqphl97b662poaca3v2gde0e2q
772595
772594
2026-09-21T00:35:38Z
Mewtow
31375
/* L'unité d'émission superscalaire */
772595
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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. Si le renommage de registre est utilisé, 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. Par exemple, si un b loc de 4 instruction bute sur une dépendance, entre la seconde etr troisième instruction, deux instruction sont émises lors d'un premier cycle, puis lesz deux restantes, et ensuite on passe au bloc suivant. 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.
La présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
iq90zfd5f1txy4w32y3kujnevq4u5cq
772596
772595
2026-09-21T00:36:02Z
Mewtow
31375
/* L'unité d'émission superscalaire */
772596
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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. Si le renommage de registre est utilisé, 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. Par exemple, si un b loc de 4 instruction bute sur une dépendance, entre la seconde etr troisième instruction, deux instruction sont émises lors d'un premier cycle, puis lesz deux restantes, et ensuite on passe au bloc suivant. 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.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
d3d3ncqa0ym4p1qwd9dtnadquxudsqq
772597
772596
2026-09-21T00:36:56Z
Mewtow
31375
/* L'unité d'émission superscalaire */
772597
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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. Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
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. Par exemple, si un b loc de 4 instruction bute sur une dépendance, entre la seconde etr troisième instruction, deux instruction sont émises lors d'un premier cycle, puis lesz deux restantes, et ensuite on passe au bloc suivant. 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.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
9ayt60dsq673ichypicw8nf0d1w20we
772598
772597
2026-09-21T00:37:11Z
Mewtow
31375
/* Les fenêtres d'instruction et stations de réservation des CPU superscalaires */
772598
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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. Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, 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 modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
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. Par exemple, si un b loc de 4 instruction bute sur une dépendance, entre la seconde etr troisième instruction, deux instruction sont émises lors d'un premier cycle, puis lesz deux restantes, et ensuite on passe au bloc suivant. 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.
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 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 le double de 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, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
05f4eaj3k9fyibnifj9qkkjq5011ahp
772599
772598
2026-09-21T00:39:09Z
Mewtow
31375
/* L'unité d'émission superscalaire */
772599
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes 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 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 le double de 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 équivalentes. Avec une fenêtre d'instruction centralisée, 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 conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
sdvttbxnwfz36x8djxz3gir8f48h5ww
772600
772599
2026-09-21T00:47:13Z
Mewtow
31375
/* L'exécution dans le désordre sur un processeur superscalaire */
772600
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
h7vww0i3d2yhomps8qqv733gwevuyub
772601
772600
2026-09-21T00:49:08Z
Mewtow
31375
/* L'exécution dans le désordre sur un processeur superscalaire */
772601
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
en99cnnov7bot2ryv50mrkdgk4xqexf
772602
772601
2026-09-21T00:49:52Z
Mewtow
31375
/* Le décodage parallèle des instructions */
772602
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
==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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
q8w283v1vb1k3lj68vqguaug0es24qd
772603
772602
2026-09-21T00:52:15Z
Mewtow
31375
/* Les problèmes du contournement sur les CPU avec beaucoup d'ALUs */
772603
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
==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. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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 interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
4nfpis2r5t9vj715xy4u8ojbowv1jcj
772604
772603
2026-09-21T00:54:15Z
Mewtow
31375
/* Le contournement sur les processeurs superscalaires */
772604
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===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]]
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. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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.
L'usage d'agglomérats peut aussi prendre en compte les interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
qvn0f092on7lb7becepc0nos1crtcq8
772605
772604
2026-09-21T01:03:09Z
Mewtow
31375
/* Le contournement sur les processeurs superscalaires */
772605
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
L'usage d'agglomérats peut aussi prendre en compte les interconnexions 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
mby2pzoak7u5spu1l3kz9merloyhp5i
772606
772605
2026-09-21T01:06:17Z
Mewtow
31375
/* Le contournement sur les processeurs superscalaires */
772606
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
L'usage d'agglomérats peut aussi prendre en compte les interconnexions 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 est de dupliquer 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.
Une autre 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
55eqqu7mlgvcsok8ibozcyjhbikz9ft
772607
772606
2026-09-21T01:11:02Z
Mewtow
31375
/* Le contournement sur les processeurs superscalaires */
772607
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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 optimisations des ports du banc de registre===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
6vh17iof52p86o9okk8o10zov9qqak5
772608
772607
2026-09-21T01:11:15Z
Mewtow
31375
/* Les optimisations des ports du banc de registre */
772608
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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 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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
0mq4mu7m8cijg3hqhq54q94h3x8bvlm
772609
772608
2026-09-21T01:13:44Z
Mewtow
31375
/* Le contournement sur les processeurs superscalaires */
772609
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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 optimisations des ports du banc de registre===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
pcvridqhzdm6e9cup459ckbzakejpjg
772610
772609
2026-09-21T01:13:58Z
Mewtow
31375
/* Le contournement sur les processeurs superscalaires */
772610
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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 optimisations des ports du banc de registre===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
fcas4m2bwkdvex7bxbw3a6ywxz8tjk5
772611
772610
2026-09-21T01:14:15Z
Mewtow
31375
/* Les optimisations des ports du banc de registre */
772611
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
===L'usage d'un cache de micro-opérations===
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
a192f0oeektrmej4gi3g5p87eds7kcq
772612
772611
2026-09-21T01:17:24Z
Mewtow
31375
/* Le front-end d'un processeur superscalaire large */
772612
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
04xlwyjkd3cjgrmd85kg3elquon7qkh
772613
772612
2026-09-21T01:17:41Z
Mewtow
31375
/* L'exécution dans le désordre sur un processeur superscalaire */
772613
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
bzqd98t6li6ymziua0vdda50ia620cl
772614
772613
2026-09-21T01:18:06Z
Mewtow
31375
/* Le front-end d'un processeur superscalaire large */
772614
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==L'exécution dans le désordre sur un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
f3oviioyoey8rmdijqm22napem8so24
772615
772614
2026-09-21T01:19:19Z
Mewtow
31375
/* L'exécution dans le désordre sur un processeur superscalaire */
772615
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
pkncgqoc5cmm07kwbfowqlcp8byg0vc
772616
772615
2026-09-21T01:19:28Z
Mewtow
31375
/* Le front-end d'un processeur superscalaire étroit */
772616
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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 d'un processeur 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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
8nzu7q3jo9ik828xq0vuo6nzns88tn1
772617
772616
2026-09-21T01:19:54Z
Mewtow
31375
/* Le front-end d'un processeur superscalaire étroit */
772617
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==L'implémentation d'un processeur superscalaire étroit==
===Le chargement de plusieurs instructions consécutives===
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 donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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 d'un processeur 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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
i060kbki056musuhggy3ocnvoo8jiss
772618
772617
2026-09-21T01:20:51Z
Mewtow
31375
/* Le chargement de plusieurs instructions consécutives */
772618
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite.
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==L'implémentation d'un processeur superscalaire étroit==
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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 d'un processeur 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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
bi5p1hgdy0ungsr2nlzsnbqq6pascob
772619
772618
2026-09-21T01:21:50Z
Mewtow
31375
/* L'impact de la superscalarité sur le front-end */
772619
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
[[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.]]
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==L'implémentation d'un processeur superscalaire étroit==
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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 d'un processeur 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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
b0vegbr0ljo5bf3ghymds85mc6cqo5m
772620
772619
2026-09-21T01:23:29Z
Mewtow
31375
/* Le chargement de plusieurs instructions consécutives */
772620
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==L'implémentation d'un processeur superscalaire étroit==
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge 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. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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 d'un processeur 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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
7dt4ss48jneqc6xap5o0apmjj7a63bz
772621
772620
2026-09-21T01:23:31Z
Mewtow
31375
/* Le décodage parallèle des instructions */
772621
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==L'implémentation d'un processeur superscalaire étroit==
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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 d'un processeur 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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
3kbpb936muniycppnhwy4s6vxdj6lte
772622
772621
2026-09-21T01:23:56Z
Mewtow
31375
/* Le décodage parallèle des instructions */
772622
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==L'implémentation d'un processeur superscalaire étroit==
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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 d'un processeur 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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
fkjnjz006er9tznhtzl1o7ozhr9ymgx
772623
772622
2026-09-21T01:23:59Z
Mewtow
31375
/* La macro-fusion : une optimisation du décodage parallèle */
772623
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==L'implémentation d'un processeur superscalaire étroit==
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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 d'un processeur 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. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
Il y a une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail. Voyons pourquoi dans le détail.
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
bf4nbvhoyh5btfkqkt8jofes5ooicef
772624
772623
2026-09-21T01:24:12Z
Mewtow
31375
/* L'implémentation d'un processeur superscalaire étroit */
772624
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
c4hh4pdksd09cuoc9qg831263vgt0fw
772625
772624
2026-09-21T01:25:26Z
Mewtow
31375
/* L'impact de la superscalarité sur l'unité d'émission */
772625
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
===L'impact de la superscalarité sur l'unité d'émission===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
epgau7sfdl4qsw1xmywxnygszlnldaw
772626
772625
2026-09-21T01:25:40Z
Mewtow
31375
/* L'impact de la superscalarité sur l'unité d'émission */
772626
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
qcn69oaer7bmhe76t01uyclr8ufbnu3
772627
772626
2026-09-21T01:26:23Z
Mewtow
31375
/* La macro-fusion : une optimisation du décodage parallèle */
772627
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
nyagrb0u604ckwuk1zpoq3sj5xtnk3n
772628
772627
2026-09-21T01:26:44Z
Mewtow
31375
/* Le banc de registre des processeurs superscalaires larges */
772628
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
===La macro-fusion : une optimisation du décodage parallèle===
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
sdmtstniv9o94qitluun4urjhz7yhvx
772629
772628
2026-09-21T01:26:53Z
Mewtow
31375
/* La macro-fusion : une optimisation du décodage parallèle */
772629
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
rihaha2st7ik114z2chif5lmvw05uxr
772630
772629
2026-09-21T01:27:13Z
Mewtow
31375
/* Les optimisations de la pile d'appel : le stack engine */
772630
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
lzivrvpv4q5sst79gyfhdxsa3vfwfwx
772631
772630
2026-09-21T01:28:37Z
Mewtow
31375
/* L’impact de la superscalarité sur les autres circuits */
772631
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre est modifiée, si elle existe, avec plus de ports de lecture/écriture.
* Le ROB a plus de ports d'écriture.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
2b3r9f9kh4ifs9mc310pzcbwcglf7j6
772632
772631
2026-09-21T01:29:36Z
Mewtow
31375
/* L’impact de la superscalarité sur les autres circuits */
772632
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
l9gamyreiuwwvx6xey6ik2n9c2993t8
772633
772632
2026-09-21T01:30:38Z
Mewtow
31375
/* L'émission multiple des accès mémoire */
772633
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une ''load/store queue'', des unités de calcul. Toute l'unité mémoire n'est pas dupliquée, dans le sens où la ''load/store queue'' reste une structure unique. Par contre, on doit rajouter des ports à la ''load/store queue''. Il faut aussi ajouter des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultannées.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Au passage, il est possible d'avoir un processeur capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter sans recourir au cache si elle tombe sur des situations de ''store to load forwarding''. Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
8hkjjx1b54jjxsrp1vn292jpupuwa7c
772634
772633
2026-09-21T01:37:36Z
Mewtow
31375
/* L'émission multiple des accès mémoire */
772634
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
* La première duplique les unités de calcul, mais pas les ports d'accès au cache. L'intérêt est que cela permet d'accumuler des accès mémoire dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. La ''load/store queue'' encaisse les accès mémoire en trop, voire peut les exécuter si elle tombe sur des situations de ''store to load forwarding''. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
* La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent.
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
srbumhz7v6mezccd7edwfhwgrxkvzwd
772635
772634
2026-09-21T01:45:05Z
Mewtow
31375
/* L'émission multiple des accès mémoire */
772635
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64.
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
tfqj8ux0qr6wl0zjqvv1ijz5osk60ud
772636
772635
2026-09-21T01:51:48Z
Mewtow
31375
/* L'émission multiple des accès mémoire */
772636
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée.
Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
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.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
5kot4o4i3j3slkqiw1djtuw1g5gj2m6
772637
772636
2026-09-21T01:53:49Z
Mewtow
31375
/* L'émission multiple des accès mémoire */
772637
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée. Idem pour l'unité d'émission et les circuits d'exécution dans le désodre, qui sont adaptés pour émettre deux instructions à la fois, mais ne sont pas dupliqués.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
Maintenant, qu'en est-il pour les autres circuits ?
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission.
Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc. Pour économiser des transistors, les ALU ne sont pas toutes dupliquées. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée.
Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
8vfi5rgyj1t8m1sfp93oswce6zblcw1
772638
772637
2026-09-21T02:13:40Z
Mewtow
31375
/* L'implémentation des processeurs superscalaires */
772638
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail.
===Les unités de calcul d'un processeur superscalaire===
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc.
Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Il est possible d'améliorer le tout en se rappelant que le multiplier est séparé de l'ALU entière, mais laissons cela de côté pour le moment.
Mais le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaison calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission.
En pratique, les CPU superscalaires sont un intermédiaires entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée.
Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
teedoo3fxytppj6zswwzgj94wmkfw4o
772639
772638
2026-09-21T02:17:22Z
Mewtow
31375
/* L'implémentation des processeurs superscalaires */
772639
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail.
===Les unités de calcul d'un processeur superscalaire===
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc.
Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Il est possible d'améliorer le tout en se rappelant que le multiplier est séparé de l'ALU entière, mais laissons cela de côté pour le moment.
Mais le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission.
En pratique, les CPU superscalaires sont un intermédiaires entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==Les unités de calcul des processeurs superscalaires étroits==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée.
Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
lcke9bv4l8msunk8lnsvi9ja71qygx4
772640
772639
2026-09-21T02:20:06Z
Mewtow
31375
/* Les unités de calcul des processeurs superscalaires étroits */
772640
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail.
===Les unités de calcul d'un processeur superscalaire===
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc.
Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Il est possible d'améliorer le tout en se rappelant que le multiplier est séparé de l'ALU entière, mais laissons cela de côté pour le moment.
Mais le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission.
En pratique, les CPU superscalaires sont un intermédiaires entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution historique des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
==Les unités de calcul des processeurs superscalaires larges==
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
===L'émission multiple des accès mémoire===
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée.
Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
3h1ojxrmvysp5uczfj2bq1oot4khbq0
772641
772640
2026-09-21T02:23:04Z
Mewtow
31375
/* Les unités de calcul des processeurs superscalaires larges */
772641
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail.
===Les unités de calcul d'un processeur superscalaire===
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc.
Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Il est possible d'améliorer le tout en se rappelant que le multiplier est séparé de l'ALU entière, mais laissons cela de côté pour le moment.
Mais le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission.
En pratique, les CPU superscalaires sont un intermédiaires entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution historique des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
===Les unités de calcul des processeurs superscalaires larges===
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
==Les ports d'émission et de décodage==
Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Il est possible de les partager, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission. L'équivalent n'existe pas sur les ports de décodage, encore que l'optimisation de macro-fusion dont on parlera plus tard pourrait en être un équivalent. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage.
===Le partage des ports d'émission et la ''port contention''===
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée.
Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
0rplhy35l6i2sogqxwqyzo98b9m968j
772642
772641
2026-09-21T02:23:58Z
Mewtow
31375
/* Les ports d'émission et de décodage */
772642
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail.
===Les unités de calcul d'un processeur superscalaire===
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc.
Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Il est possible d'améliorer le tout en se rappelant que le multiplier est séparé de l'ALU entière, mais laissons cela de côté pour le moment.
Mais le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission.
En pratique, les CPU superscalaires sont un intermédiaires entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution historique des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
===Les unités de calcul des processeurs superscalaires larges===
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
==Les ports d'émission et de décodage==
Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports.
===Le partage des ports d'émission et la ''port contention''===
Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission.
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée.
Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
4152pn4h4r51bbqyjzgrqd80in9qxs4
772643
772642
2026-09-21T02:24:07Z
Mewtow
31375
/* Les unités de calcul des processeurs superscalaires larges */
772643
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail.
===Les unités de calcul d'un processeur superscalaire===
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc.
Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Il est possible d'améliorer le tout en se rappelant que le multiplier est séparé de l'ALU entière, mais laissons cela de côté pour le moment.
Mais le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission.
En pratique, les CPU superscalaires sont un intermédiaires entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution historique des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires de la "zone grise"===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
===Les processeurs superscalaires larges===
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
==Les ports d'émission et de décodage==
Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports.
===Le partage des ports d'émission et la ''port contention''===
Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission.
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée.
Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
k6cev1qcpy73hi2cm3bjx18kacxa6l0
772644
772643
2026-09-21T02:24:28Z
Mewtow
31375
/* Les CPU superscalaires de la "zone grise" */
772644
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail.
===Les unités de calcul d'un processeur superscalaire===
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc.
Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Il est possible d'améliorer le tout en se rappelant que le multiplier est séparé de l'ALU entière, mais laissons cela de côté pour le moment.
Mais le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission.
En pratique, les CPU superscalaires sont un intermédiaires entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution historique des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires à plusieurs unités spécialisées===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
===Les processeurs superscalaires larges===
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrle shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
==Les ports d'émission et de décodage==
Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports.
===Le partage des ports d'émission et la ''port contention''===
Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission.
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée.
Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
agq9z5e7ugj4b6xrzf847ppy3gfg0bx
772645
772644
2026-09-21T02:24:49Z
Mewtow
31375
/* Les processeurs superscalaires larges */
772645
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail.
===Les unités de calcul d'un processeur superscalaire===
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc.
Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Il est possible d'améliorer le tout en se rappelant que le multiplier est séparé de l'ALU entière, mais laissons cela de côté pour le moment.
Mais le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission.
En pratique, les CPU superscalaires sont un intermédiaires entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution historique des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires à plusieurs unités spécialisées===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
Les processeurs superscalaires larges disposent d'un grand nombre d'unité de calcul. Ils ne peuvent pas se contenter des unités de calcul présentes dans un CPU non-superscalaire. Au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Ils n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrel shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
==Les ports d'émission et de décodage==
Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports.
===Le partage des ports d'émission et la ''port contention''===
Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission.
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée.
Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
gfld3o4d1y9e11cqj7fiely3e18nta8
772646
772645
2026-09-21T02:25:23Z
Mewtow
31375
/* Les CPU superscalaires à plusieurs unités spécialisées */
772646
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail.
===Les unités de calcul d'un processeur superscalaire===
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc.
Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Il est possible d'améliorer le tout en se rappelant que le multiplier est séparé de l'ALU entière, mais laissons cela de côté pour le moment.
Mais le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission.
En pratique, les CPU superscalaires sont un intermédiaires entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution historique des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires à plusieurs unités spécialisées===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrel shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
==Les ports d'émission et de décodage==
Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports.
===Le partage des ports d'émission et la ''port contention''===
Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission.
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée.
Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
noi6qiur4d96b9sw1c8yxdrgsyzxm67
772647
772646
2026-09-21T02:25:57Z
Mewtow
31375
/* Les unités de calcul d'un processeur superscalaire */
772647
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail.
===Les unités de calcul d'un processeur superscalaire===
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc.
Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission.
De plus, au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
En pratique, les CPU superscalaires sont un intermédiaires entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution historique des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires à plusieurs unités spécialisées===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses.
Une possibilité assez simple était d'émettre simultanément : deux opérations entières, une opération flottante, et un branchement. Elle mélangeait juste les techniques précédentes : double émission entière-flottante, émission parallèle des branchements, émission entière multiple. Son cout en matériel était assez faible, et se concentrait surtout sur l'unité d'émission et de chargement.
Une autre possibilité était d'émettre simultanément : trois/quatre opérations entières, une opération flottante. L'avantage était que cela permettait de mieux répartir les accès mémoire et branchements. Pour rappel, les µops mémoire et les branchements étaient exécutées comme des µops entières sur les anciens processeurs. Les branchements passaient par l'ALU, les accès mémoire passaient par une ALU entière avant d’atterrir dans l'unité mémoire. Et c'est sans oublier le multiplieur et le ''barrel shifter''. En mettant le paquet niveau émission entière, cela permettait à chaque type d'opération d'avoir son ports d'émission. De plus, les performances sont généralement très bonnes.
{|class="wikitable"
|-
! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5
|-
| ALU entière || ALU entière || ALU entière || ALU entière || FPU : opération flottante (FLOAT) |
|-
| Multiplication || Décalage || Branchement || µops mémoire || X
|}
Les deux possibilités précédentes n'introduisent pas de nouveauté particulière. On ajoute juste des ALU entières, on mélange les techniques précédentes entre elles, rien de plus. Mais deux qui vont suivre sont faites d'un autre bois.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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.
Une dernière possibilité, parfaitement compatible avec les précédentes, est l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
En combinant les techniques précédentes, les possibilités sont variées et nombreuses, trop variées. Par exemple, un processeur à pentuple émission peut émettre :
* deux opérations entières, une opération flottante, un branchement et un accès mémoire.
* deux opérations entières, deux opérations flottantes et un accès mémoire.
* trois opérations entières, une opération flottante et un accès mémoire.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec des unités. Mais le gain en performance n'en vaudrait pas la peine, surtout pour son cout en circuits. Il est rare qu'un bloc de 4 instructions contiennent exactement une opération entière, une flottante, un branchement et un accès mémoire. Il est préférable d'utiliser des unités dans un CPU triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En pratique, il y a un accès mémoire toutes les 5 instruction arithmétiques, un branchement pour 4/5 instructions arithmétiques. Donc il vaut mieux augmenter le nombre d'ALU entières avant de rajoute l'émission parallèle des branchements ou des accès mémoire. L'émission parallèle des accès mémoire est donc une sorte de signal : si un CPU superscalaire peut se la permettre, c'est signe qu'il a un budget en transistors conséquent. Et c'est signe qu'il a utilisé en priorité la double émission entière-flottante et l'émission entière multiple. Il y a bien des exceptions, comme le Pentium 4 d'Intel, et quelques autres. Mais la règle générale est que l'émission parallèle des µops mémoire s'observe pour les processeurs à 4-6 voies, pas avant.
Dupliquer des ALU entière permet d'augmenter facilement les performances pour un cout en transistor modeste. Si on souhaite modèrer le cout en transistors, seules les ALU entières proprement dites sont dupliquées, pas les multiplieurs ni le ''barrel shifter''. Mais quand on commence à approcher de 8 à 10 µops émises en même temps, les choses changent. Les ''barrels shifters'' et le multiplieur peuvent être dupliqués, en deux exemplaire, rarement trois quand on dépasse les 10 µops simultannées.
Il est 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
==Les ports d'émission et de décodage==
Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports.
===Le partage des ports d'émission et la ''port contention''===
Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission.
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée.
Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
nglyh6qc0zu684l747mfm4938tqnutd
772648
772647
2026-09-21T02:40:19Z
Mewtow
31375
/* Les CPU superscalaires à plusieurs unités spécialisées */
772648
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail.
===Les unités de calcul d'un processeur superscalaire===
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc.
Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission.
De plus, au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
En pratique, les CPU superscalaires sont un intermédiaires entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution historique des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires à plusieurs unités spécialisées===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuits associé.
Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste.
Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part.
Une autre tendance à été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
==Les ports d'émission et de décodage==
Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports.
===Le partage des ports d'émission et la ''port contention''===
Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission.
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée.
Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
3zy42xuutml5nr39bdmuxot0hnvb1xd
772649
772648
2026-09-21T02:44:14Z
Mewtow
31375
/* L'émission multiple des accès mémoire */
772649
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 multi-cycles 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.
Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 3/4 µops à la fois, alors que les larges peuvent en émettre de 6 à 10. L'intervalle entre les deux est une zone grise.
==L'implémentation des processeurs superscalaires==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre.
Intuitivement, émettre et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Il est aussi possible de dupliquer les unités de calcul pour exécuter plusieurs µops en même temps. Mais d'autres circuits sont simplement modifiés. Par exemple, l'unité de chargement n'est pas dupliquée, mais simplement modifiée, idem pour l'unité d'émission et les circuits d'exécution dans le désordre. Voyons ce qu'il en est dans le détail.
===Les unités de calcul d'un processeur superscalaire===
Intuitivement, on se dit que les unités de calcul doivent être dupliquées. Par exemple, un processeur à double émission devrait dupliquer chaque ALU et FPU en deux exemplaires (N exemplaires pour un CPU à N voies). L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est énorme ! Faire cela revient à dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, etc.
Un autre cas extrême est celui où aucune unité de calcul n'est dupliquée. Pour comprendre comment c'est possible rappelez-vous qu'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et éventuellement une unité de branchement. Cela suffit pour créer un processeur quadruple émission, très limité. Un tel processeur peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire.
Le problème est qu'il est rare que l'on ait à exécuter en même temps un calcul entier, un calcul flottant, un accès mémoire et un branchement. La quadruple émission n'est donc que rarement utilisée. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission.
De plus, au-delà de la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Reste à voir ce qui est dupliqué en pratique.
En pratique, les CPU superscalaires sont un intermédiaires entre ces deux cas extrêmes : certaines unités de calcul sont dupliquées en plusieurs exemplaires, mais pas toutes. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a 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 exécutés l'un après l'autre. Mais 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. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
En pratique, ces contraintes d’appariement ont peu d’impact. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Deux branchements consécutifs sont quant à eux plus fréquents, idem pour deux accès mémoire. Dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur et la FPU.
===Le chargement de plusieurs instructions consécutives===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. 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. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[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.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. 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.
Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité d'émission d'un processeur superscalaire===
La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la 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'''. Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais ce n'est pas systématique. De nombreux processeurs ont plus de ports d'émission que de décodage. Nous verrons pourquoi dans la suite, mais on peut dire que c'est juste plus simple et plus efficace ainsi.
Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop candidate, sous entendu "candidate à l'émission". L'unité d'émission détecte les dépendances entre la µop candidate et les µops en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en cours d'exécution dans le pipeline. mais en plus, elle doit tester les dépendances entre les µops candidates.
Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Avec une fenêtre d'instruction, il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gérent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire peut émettre 8 µops à la fois, on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Avec la superscalarité, l'unité de renommage de registre doit 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 de registre, 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.
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.]]
===Le banc de registre d'un processeur superscalaire===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. 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'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre 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. Et cette différence a une influence le nombre de ports de lecture du banc de registre.
Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec une fenêtre d'instruction, il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation (sous-entendu, après le décodage). or, comme dit plus haut, les processeurs superscalaire larges ont souvent plus de ports d'émission que de décodage.
===L’impact de la superscalarité sur les autres circuits===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes.
* L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur.
* Les décodeurs sont dupliqués, ce qui a un cout en circuit pas négligeable.
* Les circuits d'émission voient leur nombre de ports doubler/tripler/quadrupler.
* L'unité de renommage de registre, le ROB et les circuits d’exécution dans le désordre sont légèrement altérés.
* Les unités de calcul peuvent être dupliquées, mais ce n'est pas systématique.
* Le banc de registre et l'unité mémoire ne sont pas forcément modifiés, mais s'ils le sont, c'est pour leur ajouter des ports.
{|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
|}
Pour les unités de calcul, tout dépend du processeur. Il existe même des processeurs sur lesquels aucune unité de calcul n'est ajoutée ! Et pour comprendre pourquoi, nous allons parler des processeurs superscalaires historiques.
==L'évolution historique des processeurs superscalaires==
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 triple émission permettent eux d’émettre trois instructions à la fois.
Les tout premiers processeurs superscalaires avaient un budget en transistors très limité, ce qui fait qu'ils ne pouvaient pas être "totalement superscalaires". Par exemple, un processeur double émission ne pouvait pas émettre deux opérations entières en même temps, ni deux opérations flottantes, ni une opération entière avec un accès mémoire. Il y avait des paires d'instructions autorisées et des paires interdites. Les '''contraintes d'appariement''' sur les instructions à émettre en même temps étaient très strictes.
L'intérêt de ces contraintes d’appariement est que le processeur était très simple, il n'y avait pas besoin de rajouter des circuits complexes pour rendre le processeur superscalaire. Les CPU superscalaires historiques ne faisaient que mieux utiliser des unités de calcul déjà présentes. Prenons un processeur non-superscalaire simple, illustré ci-dessous. Il contient : une unité de chargement, un décodeur, une unité d'émission, une ALU, une FPU, les bancs de registre entier et flottants, et une unité mémoire. Nous avons placé l'unité mémoire à la suite de l'ALU entière, car ce sera important pour les explications qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Les processeurs superscalaires historiques modifiaient assez peu le schéma précédent. L'unité de chargement était adaptée pour lire plusieurs instructions depuis la mémoire, il y avait un seul décodeur, l'unité d'émission était assez peu modifiée. Mais une chose était certaine : aucune unité de calcul n'était dupliquée, le banc de registre n'était presque pas modifié. A la rigueur, gérer la triple 'émission avec les branchements demandait parfois d'ajouter des ports de lecture sur le banc de registre généraux, pas plus.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est 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. 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'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrètement, il suffit de charger deux instructions à la fois, scinder le décodeur en deux décodeurs plus simples (un décodeur entier et un flottant), et de modifier l'unité d'émission pour qu'elle émette deux µops à la fois. Pas besoin de dupliquer les unités de calcul, ni de toucher au banc de registres. Il faut dire que l'ALU et la FPU sont déjà séparées, comme les bancs de registres entiers/flottants. Le cout en transistors était donc minimal, pour un gain en performance certes assez limité.
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]]
La double émission entière-flottante était difficile à exploiter, mais elle pouvait permettre des gains en performance assez impressionnant. L'exemple le plus frappant étant le jeu vidéo Quake, d'IdSoftware. Le programmeur principal de ce jeu, John Carmack, a été aidé par Michaël Abrash, un nom bien connu dans le domaine de l'optimisation et du rendu 3D. Abrash a optimisé le code de Quake en utilisant directement l'assembleur. Et une bonne partie de ses optimisation visait à mixer opérations entières et flottantes de manière à utiliser au mieux la double émission entière-flottante. Plus d'informations via ce lien : [https://fabiensanglard.net/quake_asm_optimizations/index.html How Michael Abrash doubled Quake framerate].
===L'émission parallèle des branchements===
Les tout premiers CPU superscalaires utilisaient la double émission entière-flottante. C'était l'implémentation la plus simple qui soit. L'étape suivante avait aussi un cout minimal en circuit, pour un gain en performance là aussi assez limité. Là encore, l'idée était d'exploiter les unités de calcul déjà présentes dans le processeur. L'idée était d'appliquer un équivalent de la double émission, non pas pour des opérations flottantes, mais pour autre chose. Il y avait deux candidats : l'unité de branchement, l'unité mémoire. Et c'est l'unité de branchement qui a été choisie.
Le processeur pouvait émettre un branchement en parallèle d'une instruction entière. La technique s'appelle l''''émission parallèle des branchements'''. Combinée à la double émission entière-flottante, cela permettait d'émettre trois µops en même temps : un branchement, une opération flottante, et une opération entière/mémoire. Les exemples de processeurs de ce type ne sont pas très nombreux, mais on peut citer l'Intel i960CA et l'HyperSPARC. Les branchements étant fréquents, surtout sur les CPU de type RISC, les gains en performances étaient assez impressionnants. Quant au cout en transistor, il dépendait du jeu d’instruction, expliquons pourquoi.
Pour les processeurs avec un registre d'état, l'implémentation est particulièrement simple. L'unité de branchement lit ce registre d'état, sélectionne le bit adéquat, teste sa valeur, et modifie le ''program counter'' en fonction. Le processeur intègre déjà une unité de branchement séparée de l'unité de calcul, pour faire tout cela. Pas besoin d'utiliser l'unité de calcul entière, ni toute autre unité. Il n'y a même pas besoin de toucher au banc de registres, vu que le registre d'état n'est pas dedans, idem pour le ''program counter''
Et il en est de même si le processeur utiliser des registres à prédicats. Là encore, il y a un banc de registre séparé pour les prédicats et une unité de branchement dédiée. L'implémentation de la triple émission ne fait alors qu'utiliser au mieux des circuits existants.
Sur les CPU de type RISC, sans registre d'état, les choses sont cependant différentes. De tels processeurs ont des instructions de branchements qui lisent deux opérandes dans les registres, les comparent, et altèrent ou non le ''program counter'' suivant le résultat. L'implémentation de la triple émission demande alors d'ajouter deux ports de lecture au banc de registre entier, pour lire les opérandes. Il faut aussi ajouter une unité pour faire la comparaison, si elle n'était pas déjà présente. Certains CPU RISC utilisent une unité de branchement séparée, mais d'autres utilisaient l'ALU entière pour.
===L'émission multiple des micro-opérations entières===
Voyons maintenant l'étape après la triple émission entière-flottante-branchement. Elle est assez simple, à savoir qu'elle permet d'émettre deux instructions entières en même temps, avec éventuellement une instruction flottante en plus. Il est parfois possible de monter à trois ou quatre instructions entières. Le processeurs de ce type font de l''''émission multiple des micro-opérations entières'''. Nous utiliserons le terme d'''émission entière multiple'' pour simplifier les explications.
Pour cela, les unités de calcul entières doivent être dupliquées. Il doit y avoir deux ALU entières pour de la double émission, trois pour de triple émission, etc. De plus, il faut modifier le banc de registres. Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Et c'est là que les subtilités surviennent : qu'en est-il des branchements ? Des accès mémoire ? Des opérations complexes comme les multiplications et les divisions ? Pour étudier ces questions, nous allons prendre le cas d'un processeur capable d’émettre trois opérations en même temps : deux opérations entières, et une flottante, potentiellement d'autres.
Le cas des opérations complexes est assez intéressant. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les ALUs en N exemplaires. Mais ce n'est jamais fait en pratique. Concrètement, seules les ALU entières simples le sont, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ne sont pas dupliqués en N exemplaires, le cout en transistors serait trop important.
Pour un processeur à double émission, l'unité d'émission a deux ports d'émission, pour émettre deux µops entières. Les deux ports ont chacun leur ALU entière, mais l'un d'entre eux a est aussi relié au circuit multiplier. En clair, cela permet d'émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Idem pour les opérations simples comme les soustractions, les opérations bit à bit, etc. On peut en exécuter deux, ou en coupler une avec une multiplication.
Faire ainsi n'a pas de cout en performance significatif, comparé à une duplication totale des ALUs entières. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur serait inutile. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
[[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.]]
Il en est de même avec les décalages, vu que le ''barrel shifter'' n'est pas dupliqué. Il est donc possible d'émettre deux opérations de base, un décalage avec une opération de base, mais pas deux décalages en parallèle. En général, le ''barrel shifter'' et le multiplieur sont placés sur deux ports d'émission séparés. Cela permet ainsi de faire un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
Pour les branchements, deux solutions sont possibles. Avec la première, il y a émission parallèle des branchements, à savoir qu'il est possible d'émettre en branchement en parallèle de deux-trois instructions entières. Et cela impose la présence d'une unité de branchement séparée, avec son port d'émission. Avec la seconde, les branchements sont traités comme des instructions entières. Il peut y avoir une unité de branchement séparée, mais ce n'est pas souvent le cas. En général, les branchements sont exécutés dans une ALU entière.
[[File:Emission série et parallèle des branchements.png|centre|vignette|upright=2|Emission série et parallèle des branchements]]
Les accès mémoire sont traités comme des opérations entières un peu spéciales, sur les processeurs superscalaires historiques. En clair, le processeur peut émettre deux opérations entières, ou une opération entière et un accès mémoire. les accès mémoire ne viennent pas en plus des opérations entières, elles les remplacent. Les raisons à cela étaient nombreuses. Et ces raisons sont encore valables sur des petits processeurs destinés à l'embarqué ou l'informatique industrielle. Seuls les processeurs haute performance modernes font différemment.
La raison principale est le cout en circuit. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse. Pas besoin de rajouter une unité de calcul d'adresse séparée. Mais surtout, permettre l'émission parallèle des accès mémoire ferait passer un processeur triple émission à de la quadruple émission, avec tous les couts que ca implique. Il faudrait rajouter des ports de lecture/écriture au banc de registre généraux, pour traiter un accès mémoire en plus du reste. Les circuits d'émission et de chargement devraient être élargit, aussi.
Et quitte à payer un tel cout en circuit, il serait plus rentable de l'utiliser pour émettre une quatrième opération entière qu'un accès mémoire. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2.
===Les CPU superscalaires à plusieurs unités spécialisées===
Le classique deux opérations entières + une flottante était l'idéal pour la triple émission. Même les processeurs modernes à triple émission restent sur cette "norme", car elle est proche de l'optimal. Mais arrivé à la quadruple émission, les possibilités sont devenus plus diverses. L'évolution des CPU superscalaires ont évolués dans des directions différentes, chacun faisant à sa sauce, les choses sont devenus plus confuses. Le seul point commun est qu'ils ont eu tendance à rajouter des unités de calcul.
Un point important est que la quadruple émission permet de nombreuses possibilités, mais la plupart ne sont pas intéressantes. Pour donner un exemple concret, prenons un CPU superscalaire très simple, comprenant une ALU entière, une FPU, une unité de branchement, et une unité mémoire. Il est possible de créer un CPU quadruple émission avec ces unités, mais le gain en performance n'en vaudrait pas la peine pour le cout en circuits associé.
Il est rare qu'un bloc de 4 instructions contienne exactement une opération entière, une flottante, un branchement et un accès mémoire. En pratique, les seules combinaisons fréquentes sont les combinaisons calcul + accès mémoire et calcul + branchement. Les combinaisons calcul entier + calcul flottant sont aussi possibles, mais plus rares. Mais on dépasse rarement la double émission. Il est préférable d'utiliser ces unités dans un CPU double ou triple émission, en retirant l'unité de branchement et/ou en couplant l'unité mémoire à l'ALU.
En clair, une fois arrivé à la quadruple émission, on ne peut plus se contenter d'une ALU, d'une FPU, d'une unité de branchement et d'une unité mémoire. Les processeurs superscalaires larges n'ont pas le choix que de rajouter des unités de calcul, voire des unités mémoire. Et en pratique, ils ont privilégié l'ajout d'ALU entières, avant de dupliquer les autres circuits. La raison est que les programmes informatiques utilisent beaucoup plus d'additions/soustractions/comparaisons que le reste. Il y a environ 5 opérations entières de base pour une multiplication entière, le ratio est le même quand on remplace la multiplication par un branchement ou un accès mémoire. En conséquence, les ALU entières ont été dupliquées en premier, et dans de nombreux exemplaires. Reste à voir comment a été dupliqué le reste.
Les multiplieurs n'ont pas été dupliqués, sauf sur les processeurs superscalaires très larges. La raison est qu'il est rare de devoir exécuter deux multiplications à la fois. C'est la même chose pour le ''barrel shifter''. Deux branchements consécutifs sont quant à eux plus fréquents, c'est même assez courant. Aussi, les unités de branchement ont été dupliquées en second lieu, après les ALU entières. Avoir plusieurs accès mémoire proches est assez courant, surtout sur les CPU de type CISC. Aussi, les unités mémoire ont été dupliquées en troisième lieu. Pour résumer, dupliquer des ALU entières est donc la priorité, suivi par la duplication des unités de branchement, puis celle des accès mémoire, pour terminer par le multiplieur. La FPU est un peu à part.
Une autre tendance à été de séparer l'unité mémoire des ALU entières, ce qui a permis l''''émission parallèle des µops mémoire'''. Comme son nom l'indique, l'idée est de séparer les µops mémoire des opérations entières. Il devient alors possible d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière.
La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple.
La '''double émission flottante''' a été utilisée sur les processeurs Alpha. L'idée est d'émettre deux opérations flottantes à la fois. Sauf qu'une implémentation naive demanderait de dupliquer la FPU, ce qui aurait un cout en circuit rédhibitoire. Pour rappel, les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant. l'idée est d'avoir 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 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 processeurs 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|Microarchitecture Zen 1 d'AMD.]]
==Les ports d'émission et de décodage==
Les ports d'émission et de décodage sont des ressources matérielles comme les autres. Dans cette section, nous allons parler de concepts liés aux ports d'émission et de décodage. Nous allons notamment parler du partage des ports d'émission, ainsi que des relations numériques entre ces deux types de ports.
===Le partage des ports d'émission et la ''port contention''===
Il est possible de partager un port d'émission, à savoir de connecter un port d'émission à plusieurs unités de calcul. Nous en avons vu un exemple plus haut, avec l'émission multiple des opérations entières. On peut par exemple connecter un multiplieur et une ALU entière sur le même port d'émission.
Pour le moment, nous n'avons vu que des cas où un port d'émission est relié soit à des ALU entières, soit à des FPU, pas aux deux. 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é.
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.]]
Regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles, qui ne se présentent que pour certaines combinaisons de micro-opérations bien précises. 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. Mais on peut réduire la ''port contention'' en répartissant correctement les ALU sur les ports d'émission. Le choix en question dépendent fortement du fait qu'en moyenne, que certaines paires d'instructions sont plus fréquentes que d'autres. Tout cela guide ce choix de répartition des ALu sur les ports.
Nous venons de voir que la répartition des ALU sur les ports d'émission est très variable d'un processeur à l'autre. 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 réduisant la ''port contention'' de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à 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, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal répartit les ALU.
===La séparation entre largeur de décodage et d'émission===
Sur les processeurs superscalaires larges, le processeur peut émettre plus d'instructions qu'il peut en décoder. Plus haut, j'ai dit que l'unité d'émission avait parfois plus de ports d'émission que de ports de décodage. La raison est assez simple, et elle s'explique assez bien avec un exemple.
Prenons un processeur avec trois ALU entières, une FPU, une unité de branchement et une unité mémoire. Chacune a son propre port d'émission, ce qui fait que le CPU est capable d'émettre 6 µops. Du moins, c'est la théorie, car les conditions pour sont assez rares. Il faut qu'en chargeant un paquet de 6 instructions, il y ait exactement trois instructions entières, une flottante, un branchement et un accès mémoire. Si le paquet contient 5 instructions entières et un branchement, seules quatre instructions sont émises.
Dans cet exemple, dans la grande majorité des cas, seuls 4 ports seront utilisés en moyenne et deux seront sous-utilisés. Il s'agit là d'une moyenne, qui dépend du code, et des instructions exactes à exécuter. Cependant, cette moyenne nous donne un moyen d'économiser beaucoup de circuits pour un cout en performance mineur. L'idée est de ne charger que des blocs de 4 instructions, mais d'avoir 6 ports d'émission. Le processeur charge et décode 4 instructions, les quatre instructions sont envoyées sur les ports d'émission adéquat. Le cout en transistors est alors situé entre celui d'un CPU quadruple émission et sextuple émission. Et les performances sont aussi entre les deux.
Pour les performance c'est parce que cela permet d'avoir un CPU quadruple émission avec bien moins de contraintes d’appariement. Imaginez qu'on se soit limité à quatre ports d'émission : il aurait fallu mettre plusieurs unités sur le même port d'émission. En conséquence, il y aurait eu des combinaisons interdites, comme émettre deux instructions entières + un branchement + un accès mémoire ; ou deux instructions entières + une opération flottante + un accès mémoire. De tells appariement interdits sont possibles en utilisant les six ports d'émission.
==L'émission multiple des accès mémoire==
Les premiers processeurs superscalaires ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles.
Par contre, c'est autre chose sur les processeurs superscalaires larges. Vu qu'ils travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures.
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. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. La ''load/store queue'' reste une structure unique, mais on peut lui rajouter des ports. Et à ce peit jeu, il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire.
La première duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Ces processeurs pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus.
La seconde méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Le Pentium 4 était dans ce cas, mais il était un peu seul dans sa catégorie. Un cas similaire est celui où l'unité mémoire est alimentée par une fenêtre de micro-opération mélangeant instructions mémoire et entières. Les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. Dans ce cas, l'émission multiple est rarement implémentée.
Le cas suivant a une file pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. 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 file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique.
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 deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture demande d'utiliser une file d'écriture multiport, ce qui est assez compliqué. Implémenter des lectures en plus est plus simple : il suffit de rajouter une seconde file de µops LOAD et une unité de calcul, alimentées via un second port d'émission. Il est aussi possible de rajouter un second port de lecture au cache, mais ce n'est pas obligatoire.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse. De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports de lecture et d'écriture car il y a plus de lecture que d'écritures.
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettent plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
Pour résumer, l'évolution des CPU superscalaires a été la suivante :
* pipeline mémorie fusionné avec le pipeline entier ;
* séparation du pipeline mémoire du reste, avec émission parallèle des accès mémoire ;
* émission multiple, mais sans dupliquer les ports du cache ;
* émission multiple, avec émission simultanée d'une lecture et d’une écriture dans le cache ;
* émission multiple avec plusieurs accès en lecture au cache.
==Les optimisations des processeurs superscalaires larges==
Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul.
===L'étape de chargement d'un CPU superscalaire large===
Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instruction simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[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.]]
Un ''front-end'' large 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''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===Le contournement sur les processeurs superscalaires larges===
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]]
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 d’interconnexions et de comparateurs. 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 au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. 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. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un 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.
===Le banc de registre des processeurs superscalaires larges===
Sur les processeurs superscalaires larges, le banc de registre doit avoir un grand nombre de ports de lectures. Pour alimenter N ALU, il faut typiquement 2 * N ports de lecture et N ports d'écritures. Et quand N devient trop grand, le banc de registre ne suit pas. Ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeur susperscalaires larges doivent donc trouver des solutions.
Une première solution est de dupliquer 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 écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
Une autre solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec 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.
==La macro-fusion : une optimisation du décodage parallèle==
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. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. 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 n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''stack engine'' : une optimisation de la pile d'appel==
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.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
sg0c2b8msti9lkkwym9jkvkzil3aw2d
Utilisateur:Simon Villeneuve/Wikipédia en éducation/Introduction
2
70008
772550
613953
2026-09-20T22:32:48Z
Simon Villeneuve
24400
/* Paradis du lecteur, enfer du contributeur */ correction lien
772550
wikitext
text/x-wiki
== Introduction ==
=== Wikipédia, l'éléphant en éducation ===
{{citation bloc|L'utilisation de Wikipédia à des fins académiques semble être source de tensions, de calculs et de dissimulations.|Gilles Sahut et André Tricot|Les élèves, les enseignants et Wikipédia : je t’aime, moi non plus, moi quand même<ref name="SahutetTricot"/>}}
{{citation bloc|Prôner l'interdiction de Wikipédia comme source d'information auprès de ses élèves est aussi efficace que prôner l'abstinence comme moyen de contraception<ref group="note">Idée semblable exprimée sur <code>[//medium.com/@jakeorlowitz/things-my-professor-never-told-me-about-wikipedia-561d669af9b0 medium.com/@jakeorlowitz/things-my-professor-never-told-me-about-wikipedia-561d669af9b0]</code>.</ref>.}}
L'encyclopédie en ligne <code>[[w:Wikipédia|Wikipédia]]</code> (wikipedia.org) est un projet d'<code>[[w:savoir libre|encyclopédie libre]]</code> fondé en 2001 et qui compte près de 300 versions linguistiques. Ces dernières sont alimentées chaque jour par plus de cent mille contributeurs à travers le monde. Elles sont visitées chaque mois par près de 500 millions d'internautes, ce qui classe le site, depuis le milieu des années 2000, parmi les 10 sites les plus visités de la planète. Au début des années 2010, le site propose {{citation|plus de 29 millions d'articles dans plus de 280 langues. Plus de {{formatnum:25000}} articles sont créés par jour sur les différentes versions linguistiques de Wikipédia et on y compte plus de 10 millions de modifications par mois}}<ref>{{Lien web |id= | titre=Livret Bienvenue sur Wikipédia|langue=fr |auteur=Bookshelf project (translated by [[Wikimédia France]])|url=https://fr.wikipedia.org/wiki/Aide:Livret |site=fr.wikipedia.org |passage=2 & 3|consulté le={{1er}} septembre 2014}}</ref>.
[[File:Wikipedia Page views deWP enWP frWP ruWP esWP jaWP 2008-2014.jpg|thumb|upright=2.5|center|Fréquentation des articles de Wikipédia (2008 à 2014) selon les versions linguistiques. La courbe de Wikipédia en français est en turquoise et oscille actuellement aux environs de 700 millions par mois au cours de l'année scolaire<ref><code>[http://laboratoire.agencedunumerique.gouv.fr/2017/07/10/lon-sait-usages-de-wikipedia-france/ laboratoire.agencedunumerique.gouv.fr/2017/07/10/lon-sait-usages-de-wikipedia-france/]</code></ref><ref><code>[//tools.wmflabs.org/siteviews/?platform=all-access&source=pageviews&agent=user&start=2016-07&end=2017-06&sites=fr.wikipedia.org tools.wmflabs.org/siteviews/?platform=all-access&source=pageviews&agent=user&start=2016-07&end=2017-06&sites=fr.wikipedia.org]</code></ref><ref><code>[//analytics.wikimedia.org/dashboards/vital-signs/#projects=frwiki/metrics=Pageviews analytics.wikimedia.org/dashboards/vital-signs/#projects=frwiki/metrics=Pageviews]</code></ref>. Vous pouvez voir les versions linguistiques les plus consultées par pays ainsi que le nombre moyen de consultations mensuelles par habitant à l'adresse <code>[//stats.wikimedia.org/wikimedia/animations/wivivi/wivivi.html stats.wikimedia.org/wikimedia/animations/wivivi/wivivi.html]</code>.]]
Fortement citée dans le web<ref name="Demartini"/>, l'encyclopédie est l'un des sites les plus consultés dans le domaine de l'éducation. Au Canada, elle est le site le plus visité dans le domaine, plus de six fois plus que ''<code>[[w:Yahoo! Questions/Réponses|Yahoo! Questions/Réponses]]</code>'', deuxième site en tête de liste<ref>{{article|langue=en|auteur=Siyoung Chung|titre=Les facteurs cognitifs et sociaux déterminant l'utilisation de Wikipedia et la recherche d'information|date=automne 2012|volume=38|numéro=3|périodique=La Revue canadienne de l'apprentissage et de la technologie|url texte=http://cjlt.csj.ualberta.ca/index.php/cjlt/article/download/654/352}}</ref>. 38 à 40 % des adolescents utilisent Wikipédia tous les jours ou plusieurs fois par semaine<ref>{{de}}/{{en}}JIM 2014: Jugend, Information, (Multi-) Media.
Medienpädagogischer Forschungsverbund Südwest. Stuttgart, November 2014 [http://www.mpfs.de/fileadmin/JIM-pdf14/JIM-Studie 2014.pdf PDF] (in German, with English summary)</ref><ref>{{de}}/{{en}}JIM-STUDIE 2013. Jugend, Information, (Multi-) Media. Medienpädagogischer Forschungsverbund Südwest, 2013 [http://www.mpfs.de/fileadmin/JIM-pdf13/JIMStudie2013.pdf PDF] (in German, with English summary)</ref>. Les trois quarts des étudiants américains utilisent Wikipédia pour faire leurs devoirs<ref>{{lien web|url=http://etudiant.lefigaro.fr/le-labeducation/actualite/detail/article/wikipedia-le-nouveau-manuel-scolaire-mondial-392/|titre=Wikipédia, le nouveau manuel scolaire mondial|auteur=Assma Maad|date=08 novembre 2012}}</ref>. Encore grandement diabolisée par plusieurs intervenants en éducation<ref group="note">Selon une enquête réalisée en France, « [...] les jeunes sont exposés à des discours dépréciatifs à l’égard de cette ressource. 67,9% des répondants ont déclaré avoir entendu ou lu des critiques négatives à son propos. Plus de la moitié ont affirmé avoir déjà entendu un de leurs enseignants formuler une critique négative à l’égard de Wikipédia et seulement un cinquième d’entre eux des appréciations positives. ».</ref><ref><code>[http://www.adjectif.net/spip/spip.php?article420 www.adjectif.net/spip/spip.php?article420]</code></ref><ref><code>[http://www.ledevoir.com/societe/medias/461170/wikipedia-et-l-histoire-du-quebec www.ledevoir.com/societe/medias/461170/wikipedia-et-l-histoire-du-quebec]</code></ref><ref name="CNRS"><code>[[:n:fr:France : le CNRS organise une journée sur Wikipédia|n:France : le CNRS organise une journée sur Wikipédia]]</code></ref><ref><code>[http://vismaviedejeuneprof.over-blog.com/2015/10/les-profs-et-wikipedia vismaviedejeuneprof.over-blog.com/2015/10/les-profs-et-wikipedia]</code></ref><ref name="SahutetTricot">{{lien web|url=http://andre.tricot.pagesperso-orange.fr/SahutTricot LesCahiers 2013.pdf|éditeur=Cahiers Pédagogiques, n° 508|année=2013|titre=Les élèves, les enseignants et Wikipédia : je t’aime, moi non plus, moi quand même|auteur=Gilles Sahut et André Tricot}}</ref><ref name="lycéens"><code>[http://rue89.nouvelobs.com/2015/06/01/les-lyceens-font-pomper-wikipedia-ils-contribuent-aussi-259484 rue89.nouvelobs.com/2015/06/01/les-lyceens-font-pomper-wikipedia-ils-contribuent-aussi-259484]</code></ref><ref><code>[//theconversation.com/wikipedia-pour-une-critique-pertinente-69110 theconversation.com/wikipedia-pour-une-critique-pertinente-69110]</code></ref>, l'encyclopédie libre est cependant devenue incontournable<ref>https://edtechmagazine.com/higher/article/2017/12/wikipedia-trustworthy-academic-resource-scientists-think-so</ref>. Ainsi, '''87 % des professeurs''' américains utilisent le site<ref>{{lien web|url=http://etudiant.lefigaro.fr/le-labeducation/actualite/detail/article/les-profs-utilisent-wikipedia-autant-que-leurs-eleves-1348/|titre=Les profs utilisent Wikipédia autant que leurs élèves|auteur=Assma Maad|date=04 mars 2013|site=http://etudiant.lefigaro.fr|éditeur=''Le Figaro''|consulté le=5 février 2015}}</ref>. Considérant sa vaste consultation à la fois par les étudiants et les enseignants de toute discipline et malgré le fait que '''Wikipédia n'est pas un ouvrage de référence'''<ref group="note">Par exemple, Wikipédia ne peut pas être utilisée comme référence pour sourcer d'autres articles de Wikipédia <small>(<code>[[w:Wikipédia:Citez vos sources#Wikipédia ou tout wiki n'est pas une source|WP:Citez vos sources#Wikipédia ou tout wiki n'est pas une source]]</code>)</small>. Voir aussi <code>[[w:Wikipédia:Le_Bistro/8_juillet_2017#Wikip.C3.A9dia_n.27est_pas_une_source_scientifique|WP:Le Bistro/8 juillet 2017#Wikipédia n'est pas une source scientifique]].</code></ref><ref><code>[[:meta:Research:Newsletter/2015/February#Assignment designed to convince students of Wikipedia.27s .22fundamental untrustworthiness.22 achieves the opposite]]</code></ref>, tout acteur sérieux du monde de l'éducation s'intéresse à son fonctionnement.
[[File:2012 coloriage elephant Helmer Mathias.png|right|200px]]
Bien que plusieurs personnalités de milieux divers encouragent l'appropriation de l'encyclopédie libre par les intervenants en éducation<ref>{{Lien web
| auteur = Christian Vandendorpe
| titre = Wikipédia: un cadeau planétaire | Affaires Universitaires
| jour = 8
| mois = avril
| année = 2015
| url = http://www.affairesuniversitaires.ca/articles-de-fond/article/wikipedia-un-cadeau-planetaire/
| site = Affaires Universitaires
| en ligne le =
| consulté le = 9 avril 2015
}}</ref><ref><code>[http://www.ledevoir.com/societe/medias/460859/le-quebec-parent-pauvre-de-wikipedia www.ledevoir.com/societe/medias/460859/le-quebec-parent-pauvre-de-wikipedia]</code></ref><ref>{{Lien web
| auteur = Stéphane Baillargeon
| titre = Médias - Wikipoche | Le Devoir
| jour = 17
| mois = janvier
| année = 2011
| url = http://www.ledevoir.com/societe/medias/314795/medias-wikipoche
| site = Le Devoir
| en ligne le =
| consulté le = 9 avril 2015
}}</ref><ref>{{Lien web|url=http://www.letudiant.fr/educpros/opinions/plaidoyer-pour-enseigner-wikipedia.html|titre=Plaidoyer pour enseigner Wikipédia|date=24 novembre 2016|auteur=Alexandre Hocquet}}</ref><ref>{{Lien web
| auteur = FACIL
| titre = L'informatique libre dans l'enseignement supérieur et la recherche
| jour = 22
| mois = février
| année = 2013
| url = https://facil.qc.ca/sites/facil.qc.ca/files/memoire-de-facil-pour-le-sommet-sur-l-enseignement-superieur-22-fev-2013_0.pdf
| site =
| en ligne le =
| consulté le = 9 avril 2015
}}, page 13</ref>, le nombre de ces derniers contribuant de manière active au projet francophone est limité. <!--Alors que d'autres communautés linguistiques forment des intervenants en éducation<ref>https://blog.wikimedia.org/2015/11/02/wikimedia-serbia-professional-development/</ref><ref>https://blog.wikimedia.org/2014/06/10/israels-ministry-of-education-wikimedia-israel-agree-on-new-unique-initiative/</ref>, intègrent Wikipédia dans le cursus scolaire<ref>http://etudiant.lefigaro.fr/le-labeducation/actualite/detail/article/ecrire-des-articles-sur-wikipedia-pour-obtenir-sa-licence-1506/</ref><ref>https://blog.wikimedia.org/2013/05/02/wikipedia-education-program-arab-world/</ref>, voire érigent des <code>[[w:Monument à Wikipédia|statues en l'honneur du projet]]</code>, en français, -->Wikipédia demeure <code>[[:wikt:l'éléphant dans la pièce|l'éléphant dans la pièce]]</code><ref group="note">Cette image est reprise dans https://www.academia.edu/26337149/Bridging_the_Gap_Between_Wikipedia_and_Academia</ref>. Les raisons de cette situation sont probablement multiples. L'absence d'un manuel joue certainement un grand rôle. Comme l'encyclopédie libre se distingue à plusieurs niveaux des encyclopédies classiques, il faut du temps et de l'énergie pour apprendre à bien l'utiliser et surtout à y contribuer. Précisons seulement, pour le moment, que des études montrent que la perception de la fiabilité de l'encyclopédie libre augmente avec la connaissance de ses rouages<ref>https://outreach.wikimedia.org/wiki/Education/News/October_2017/Hundred_teachers_trained_in_the_Republic_of_Macedonia</ref>. Une fois l'éléphant apprivoisé, il peut devenir un formidable outil éducationnel.<br />Lorsqu'ils sont interrogés sur le sujet, 64 % des enseignants manifestent un certain intérêt envers la publication d'un manuel d'utilisation de Wikipédia (''catalogue presenting best practices''<ref>{{article| last1 = Meseguer Artola| first1 = Antoni| last2 = Aibar Puentes| first2 = Eduard| last3 = Lladós Masllorens| first3 = Josep| last4 = Minguillón Alfonso| first4 = Julià | last5 = Lerga Felip| first5 = Maura| title = Factors that influence the teaching use of Wikipedia in Higher Education| date = 2014-12-11| url = http://openaccess.uoc.edu/webapps/o2/handle/10609/39441}}</ref>).
[[File:AllWiki.png|thumb|center|upright=2.5|Proportion des <code>[[w:Nom de domaine|domaines]]</code> pointant vers Wikipédia (début 2015). Des 36 millions de liens recensés, la moitié proviennent du domaine .com et pointent vers Wikipédia en anglais.<br />80 % des liens pointent vers Wikipédia en anglais, alors que le 20 % qui reste pointe vers Wikipédia en espagnol, indonésien, allemand, français, italien et portugais.<br />Les domaines francophones sont représentés à droite, au centre<ref name="Demartini"><small>'''(en)'''</small> Gianluca Demartini. « [//blog.wikimedia.org/2015/02/03/who-links-to-wikipedia/ Who links to Wikipedia?] », ''WMF blog'', 3 février 2015.</ref>.]]
=== Paradis du lecteur, enfer du contributeur ===
[[File:Long tail.svg|vignette|Le mode de fonctionnement des wikis publics permet d'exploiter l'effet de <code>[[w:Longue traîne|longue traîne]]</code> (en jaune sur le graphique). Ainsi, par exemple, là ou d'autres encyclopédies numériques proposent des dizaines de milliers d'articles, Wikipédia, toutes langues confondues, en propose des ''dizaines de millions''.]]
{{citation bloc|''Ring the bells that still can ring<br>Forget your perfect offering<br>There is a crack in everything<br>That's how the light gets in.''|<code>[[w:Leonard Cohen|Leonard Cohen]]</code>|Anthem<ref><code>[http://www.leonardcohenfiles.com/album10.html#78 www.leonardcohenfiles.com/album10.htmll#78]</code></ref>}}
{{citation bloc|...seul le pénitent pourra le passer...<br>...seul le pénitent pourra le passer...<br>...le pénitent pourra le passer...|Henry Jones Sr. et Jr.|<code>[[w:Indiana Jones et la Dernière Croisade|Indiana Jones et la Dernière Croisade]]</code><ref><code>[//www.youtube.com/watch?v=NkGTyndJC1w www.youtube.com/watch?v=NkGTyndJC1w]</code></ref>}}
Wikipédia est un <code>[[w:wiki|wiki]]</code> public sous licence libre, c'est à dire un site que tout le monde peut modifier en <code>[[w:temps réel|temps réel]]</code>. En conséquence, <u>tout</u> ce qui est fait sur Wikipédia est public, réutilisable... et discutable. Puisque la presque totalité des contributions faites sur Wikipédia sont réalisées par des auteurs anonymes ou sous pseudonymes, l'évaluation du travail ne se fait pas en fonction de l'autorité ou de la crédibilité des auteurs, mais de l'autorité ou de la crédibilité des sources citées.
[[Fichier:Évolution de l'article Pomme.ogv|vignette|Évolution de l'article <code>[[w:pomme|pomme]]</code> de Wikipédia en français. Wikipédia est ''dynamique'' et une entrée peut évoluer très rapidement selon différents critères, notamment l'actualité<ref group="note">Pour avoir une autre idée de l'évolution dynamique d'un article de Wikipédia, voyez [//d2chgkz0kdtxdm.cloudfront.net/wp-content/uploads/2015/06/animation-full.gif cette vidéo] des modifications effectuées au cours des premières heures de l'article de Wikipédia en anglais ''[[:w:en:Emanuel African Methodist Episcopal Church]]''.</ref>.]]
<code>[[w:Àmha|Àmha]]</code>, cette méthode de fonctionnement est avantageuse pour le lecteur. Elle lui permet d'avoir accès à un contenu ''dynamique'', mis à jour périodiquement et qui peut être amélioré par des auteurs de tous horizons<ref group="note">Vous pouvez avoir une idée de l'évolution dynamique de Wikipédia en visitant le site <code>[http://wikistream.wmflabs.org/ wikistream.wmflabs.org]</code>. Par défaut, le site présente les modifications de l'ensemble des Wikipédia faite dans tous les espaces par tous les groupes d'utilisateurs. Vous pouvez sélectionner le visionnement des modifications en fonction de la version linguistique, de l'espace et/ou du groupe d'utilisateur désiré.</ref>. En canalisant une certaine énergie bénévole issue du web, le wiki public permet d'avoir accès à des notions qui sont peu ou pas abordées par d'autres sources éditoriales facilement accessibles. Enfin, en faisant en sorte de limiter les censures de toutes sortes ainsi que les <code>[[w:argument d'autorité|arguments d'autorité]]</code>, l'anonymat permet une certaine liberté rédactionnelle et vise à concentrer l'attention des artisans du site entièrement sur le contenu et ses sources, plutôt que sur la perception de l'autorité ou de la crédibilité des utilisateurs impliqués<ref name="Barbe"/>.
[[File:Hercules holding Wikipedia.jpg|thumb|Un contributeur de Wikipédia doit accepter que le travail sera toujours inachevé.]]
Si elle est avantageuse pour le lecteur, cette méthode est cependant extrêmement exigeante pour le contributeur. Elle implique que ce dernier adopte une méthode de <code>[[w:travail collaboratif|travail collaboratif]]</code>, dont l'une des bases implique d'abandonner le ''contrôle'' de « son » travail, qui devient « le » travail. Cette manière de faire est loin d'être « naturelle » chez tous et chacun<ref group="note">Ainsi, le trois quart des universitaires n'aimeraient pas que l'on modifie leur « brouillon » (''papers-in-progress'') et un quart d'entre-eux ne sont toujours pas à l'aise de ne pas avoir le contrôle de « leur » texte après publication.</ref><ref>''<code>[[w:Wikipédia:RAW/2014-05-30#WesternOntario|Academic opinions of Wikipedia and open-access publishing]]</code>''</ref>.
Sur un wiki public, le travail fait peut constamment être remis en cause par n'importe qui. Il peut prendre une forme ultérieure très éloignée de l'idée de base, voire être supprimé partiellement ou totalement<ref group="note">Cela concerne tous les aspects du site. Ainsi, par exemple, vous pouvez voir dans la discussion « Suppression de catégories » initiée sur la page de l'utilisateur Thierry Caro (<code>[[w:Spécial:Diff/121626247|Spécial:Diff/121626247]]</code>), un contributeur expérimenté reprocher à un administrateur la suppression de catégories qu'il a créées.</ref>. Si le contributeur désire que son travail demeure en ligne, il doit pouvoir non seulement répondre de ce dernier, mais également en accepter l'évolution. Cette dernière dépend des règles et recommandations de Wikipédia et des différents consensus qui s'établissent entre les contributeurs au fil du temps.
En plus de devoir accepter cette méthode de travail, le contributeur doit également adapter ses rapports sociaux en fonction de l'anonymat. Cela fait en sorte, entre autres, que, contrairement à d'autres aspects de la <code>[[w:vraie vie|vraie vie]]</code>, le contributeur ne peut pas justifier son travail en affirmant quelque chose du genre « j'ai tel ou tel diplômes/qualifications/expériences dans le domaine, alors je sais ce que je fais ». Cet argument est également vrai dans le sens contraire : le contributeur ne peut pas présumer de la compétence ou non-compétence de son interlocuteur en fonction de son « nom ». Cela fait en sorte, notamment, que lorsqu'il y a divergence d'opinions entre contributeurs, la seule manière de trancher est de soutenir son argumentaire à l'aide de <code>[[w:Wikipédia:Citez vos sources|sources externes]]</code> (ouvrages sur les données disputées). Plus les sources externes fournies par un contributeur sont de qualité, plus il y a de chance que l'on atteigne un <code>[[w:Wikipédia:Consensus|consensus]]</code>. En général, lorsqu'un contributeur a une formation dans un domaine particulier, il est capable d'argumenter et de produire les sources justifiant son travail. Cependant, tout cela est <code>[[w:chronophage|chronophage]]</code> et, une fois qu'il a convaincu le ou les autres contributeurs concernés par la divergence de <code>[[w:point de vue cognitif|point de vue cognitif]]</code> (''PoV''), il est possible que le même travail de justification soit à recommencer avec de nouveaux contributeurs plus tard, surtout si le travail réalisé est le moindrement sujet à interprétations.
En conséquence, contribuer sur Wikipédia est un exercice constant d'humilité<ref name="Barbe"><code>[//books.openedition.org/pupo/4115 books.openedition.org/pupo/4115]</code></ref><ref name="CNRS"/>. Il n'est pas possible d'y considérer un travail comme stable et terminé et il faut parfois accepter de lâcher prise, en ayant confiance que les modifications ultérieures apportées à son travail bonifieront ce dernier. Cela ne veut pas dire qu'il faille laisser des utilisateurs <code>[[w:Wikipédia:Vandalisme|vandaliser]]</code> son travail. Cela veut dire qu'il faut accepter de discuter à propos de ce dernier et que si plusieurs utilisateurs différents adoptent une vision divergente de la vôtre sur l'évolution <s>de votre</s> « du » travail, il faut « <code>[[:wikt:lâcher prise|lâcher prise]]</code> » et accepter cette dernière<ref group="note">Dans le domaine de l'édition classique, personne ne supprimera votre travail. Cependant, on pourra refuser de publier ce dernier et le résultat sera le même.</ref>.
{{citation bloc|"Avoir raison" ne suffit pas sur Wikipédia : il faut en convaincre les autres. Ce que l'on ne parvient pas toujours à faire : auquel cas, il faut s'y résoudre et passer à autre chose, en espérant que le temps fasse son œuvre.''|<code>[[w:utilisateur:Gede|gede]]</code>|Départ de <code>[[w:utilisateur:Vol de nuit|Vol de nuit]]</code><ref>{{lien web|date=28 septembre 2009 à 8h18 CET|auteur=[[w:utilisateur:Gede|gede]]|url=http://fr.wikipedia.org/w/index.php?title=Wikip%C3%A9dia:Le_Bistro/28_septembre_2009&diff=45240597&oldid=45240559|éditeur=''Le Bistro''|titre=Départ de Vol de nuit}}</ref>}}
=== Communauté de Wikipédia en français ===
Les projets hébergés par la WMF laissent une très grande place à l'initiative individuelle<ref group="note">Sur Wikipédia en français, cette place est illustrée notamment par la règle « <code>[[w:WP:NHP|N'hésitez pas !]]</code> » ainsi que le cinquième principe fondateur (Wikipédia n'a pas d'autres règles fixes).</ref>. Cependant, les projets sont ultimement gérés par les communautés qui les constituent. Ainsi, en cas de conflit quelconque, la communauté d'un projet aura toujours le dernier mot. En conséquence, pour savoir ce que l'on retrouvera ou pas sur un wiki particulier hébergé par la WMF et ce que l'on peut y faire ou pas, il faut connaître sa communauté. Ainsi, chaque wiki hébergé par la WMF possède ses propres règles. Voilà pourquoi il faut toujours préciser que les wikis ''de'' la WMF sont '''hébergés''', et non gérés, par cette dernière.
En général, malgré l'anonymat de la presque totalité des contributeurs, toutes les communautés de projets finissent par fonctionner selon une certaine forme de <code>[[w:méritocratie|méritocratie]]</code>. Ainsi, tous les contributeurs ne sont pas égaux et le « poids » accordé à leurs propos, le « regard » posé sur leurs actions ainsi que les actions prises à leur égard, sont fortement influencés par leur réputation acquise au fil du temps. Des chercheurs américains estiment qu'une forme de <code>[[w:Règle du 1 %|règle du 1 %]]</code> émerge sur le site et que le 1 % des contributeurs les plus actifs sur Wikipédia en anglais a créé environ 80 % du contenu<ref>{{lien web|url=http://www.purdue.edu/newsroom/releases/2017/Q4/results-of-wikipedia-study-may-surprise.html|titre=Results of Wikipedia study may surprise|date=6 novembre 2017|consulté le=7 novembre 2017}}</ref>.
En particulier, depuis sa fondation en mars 2001, la communauté de <code>[[w:Wikipédia en français|Wikipédia en français]]</code> (fr.wikipedia.org) a développé des caractéristiques qui lui sont propres. En novembre 2015, on y recense environ {{formatnum:110000}} comptes utilisateurs ayant modifié au moins dix fois le projet<ref name="statswikipediens"/>. Le site compte environ {{formatnum:15000}} contributeurs actifs (ayant réalisé au moins une contribution au cours du dernier mois), dont plus de {{formatnum:4000}} ayant fait au moins 5 contributions au cours du dernier mois et environ 800 ayant fait plus de 100 contributions durant cette période<ref><code>[[w:Wikipédia:Statistiques|WP:Statistiques]]</code></ref><ref><code>[//stats.wikimedia.org/EN/ReportCardTopWikis.htm#lang_fr stats.wikimedia.org/EN/ReportCardTopWikis.htm#lang_fr]</code></ref><ref name="statswikipediens"><code>[//stats.wikimedia.org/FR/ChartsWikipediaFR.htm#1 stats.wikimedia.org/FR/ChartsWikipediaFR.htm#1]</code></ref><ref name="Statsgenerales"><code>[//stats.wikimedia.org/FR/Sitemap.htm stats.wikimedia.org/FR/Sitemap.htm]</code></ref>. On estime que 440 comptes utilisateurs ont produit 44 % des mots persistants sur Wikipédia en français, ce qui signifie environ 2 personnes par million de francophones<ref><code>[[n:fr:France_:_rencontres_Wikimedia_sur_l%27%C3%A9ducation#Prospective|France : rencontres Wikimedia sur l'éducation#Prospective]]</code></ref>.
[[File:Pourcentage de wikipédiens par pays de résidence - secteurs - août06.jpg|right|300px]]
Au début des années 2010, on estime que la communauté de Wikipédia en français est constituée d'environ 80 % de Français, 6 % de Belges, 5 % de Canadiens, 3 % de Suisses, 1 % d'Américains et de 5 % de contributeurs provenant des autres pays du monde<ref><code>[//stats.wikimedia.org/wikimedia/squids/SquidReportPageEditsPerLanguageBreakdown.htm stats.wikimedia.org/wikimedia/squids/SquidReportPageEditsPerLanguageBreakdown.htm]</code></ref><ref><code>[http://www.wikimedia.fr/sites/default/files/Rapport_de_WMFR_sur_l%27utilisation_de_la_langue_fran%C3%A7aise.pdf La place des projets Wikimedia en français dans l'environnement linguistique mondial de l'internet]</code>, p. 16</ref>. Les contributeurs sont généralement jeunes<ref><code>[[w:Wikipédia:Répartition des wikipédiens|WP:Répartition des wikipédiens]]</code></ref>, « occidentaux, éduqués, industrialisés, riches et démocrates » (''Western, educated, industrialized, rich and democratic'', <code>[[w:Weird#WEIRD|WEIRD]]</code>) et leur culture s'approche de celle des « protestants anglo-saxons blancs » (''White Anglo-Saxon Protestant'', <code>[[w:White Anglo-Saxon Protestant|WASP]]</code>)<ref name="Wathelet"/>. Beaucoup plus à l'aise avec l'informatique que l'internaute moyen, plusieurs wikipédiens sont des adeptes, notamment, de l'''<code>[[w:Internet Relay Chat|Internet Relay Chat]]</code>'' (IRC), un système de communication en ligne textuelle instantanée publique et privée dont plusieurs canaux sont dédiés aux projets de la WMF.
On note dans la communauté un <code>[[meta:Fossé des genres|fossé des genres]]</code> flagrant dont l'ampleur est difficile à déterminer<ref group="note">D'après la dernière enquête M@rsouin, 87 % des contributeurs de Wikipédia en français sont masculins</ref>. Enfin, il existe d'autres particularités secondaires de la communauté, telles la surreprésentation d'autistes<ref><code>[[w:Wikipédia:Contributeurs autistes sur Wikipédia|WP:Contributeurs autistes sur Wikipédia]]</code></ref> et de propriétaires de chats<ref><code>[[w:Wikipédia:Wikipédiens par animal domestique|WP:Wikipédiens par animal domestique]]</code></ref>.
Puisque le fonctionnement du projet est principalement déterminé par ses participants, tous ces éléments teintent fortement le contenu et le fonctionnement de Wikipédia en français. Ainsi, par exemple, on compte beaucoup plus de sujets français traités que de sujets québécois, et on compte beaucoup plus de sujets québécois que de sujets africains<ref><code>[http://geography.oii.ox.ac.uk/?page=the-geographically-uneven-coverage-of-wikipedia geography.oii.ox.ac.uk/?page=the-geographically-uneven-coverage-of-wikipedia]</code></ref>. Ajoutons que le <code>[[w:Registre de langue|registre de langue]]</code> reconnu est le <code>[[w:Français de France|français de France]]</code> et que l'heure affichée dans l'<code>[[w:Aide:Historique|historique des pages]]</code> est par défaut l'<code>[[w:Heure normale d'Europe centrale|heure centrale européenne]]</code> (''Central European Time'', CET).
[[File:Geotagged articles wikimap RENDER fr small.png|thumb|upright=2|center|Articles géolocalisés de Wikipédia en français (2012).]]
Après avoir connu une augmentation exponentielle de 2003 à 2009, la communauté de Wikipédia en français est désormais relativement stable. Elle s'est dotée de plusieurs règles et outils pour délimiter et modeler le contenu du site. Ces règles et outils ne sont pas fixes et évoluent au gré des initiatives personnelles ou collectives ainsi qu'au cours des discussions communautaires. Une partie de cet ouvrage est consacrée à la description des différents us et coutumes du projet afin d'y faciliter votre insertion.
[[File:Active Editors frwiki.pdf|thumb|upright=2|center|Le nombre de contributeurs « actifs » de Wikipédia en français est relativement stable depuis 2009.]]
=== Guide ===
Cet ouvrage vous indique comment apprivoiser les <code>[[w:Wikipédia:Principes fondateurs|principes fondateurs]]</code> du projet. Il explique quels parcours consultatifs et contributifs sont les plus appropriés selon vos besoins. Ces parcours sont présentés par ordre croissant de difficulté, selon quatre pôles d'exploration : comment '''naviguer''', '''étudier''', '''contribuer''' et '''enseigner''' avec Wikipédia. Chaque pôle est construit sur les acquis du niveau précédent et il est fortement conseillé de maîtriser la plus grande partie d'un pôle avant de passer au suivant.
Le guide expose des démarches, procédures, scénarios et exercices pas-à-pas qui vous permettront d'augmenter peu à peu votre maîtrise du site et d'évaluer votre niveau de progression dans la maîtrise de l'outil, d'abord de manière individuelle consultative, puis de manière individuelle contributive et, enfin, de manière collective consultative et contributive en salle de classe ou lors d'ateliers. Il donne aussi des conseils sur les choses à faire et à ne pas faire pour bien s'insérer dans une dynamique de travail collaboratif en général et sur Wikipédia en français en particulier.
k3bmhskb8vf35zr6i8zbvdpgjv5ydj3
Mathc initiation/a582
0
81064
772657
772487
2026-09-21T09:47:28Z
Xhungab
23827
772657
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a584#* Les suites usuelles :| Sommaire]]
{{Partie{{{type|}}}|Transformées en Z usuelles, des signaux causales discrets :}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal : ''' '''Transformée en Z : ''' Domaine de convergence
'''f(n)''' '''F(z)'''
'''δ(n) = 1''' '''1''' C '''* L'impulsion unitaire'''
'''u(n) = 1''' '''[[Mathc initiation/0070#L'échelon unitaire : u(n) = 1|z/(z-1)]]''' |z| > 1 '''* L'échelon unitaire'''
'''r(n) = n''' '''[[Mathc initiation/0070#La rampe : r(n) = n|z/(z-1)^2]]''' |z| > 1 '''* La rampe'''
'''c(n) = n^2''' '''[[Mathc initiation/0070#Le carré : c(n) = n^2|z(z+1)/(z-1)^3]]''' |z| > 1 '''* Le carré'''
'''f(n) = a^n''' '''[[Mathc initiation/0070#L'exponentiel : f(n) = a^n|z/(z-a)]]''' |z| >|a| '''* L'exponentiel'''
'''g(n) = cos(kn)''' '''[[Mathc initiation/0071#La fonction cosinus discret : g(n) = cos(kn)|[z^2-z cos(k)]/[z^2-2z cos(k)+1] ]]''' |z| > 1 '''* La fonction cosinus discret'''
'''h(n) = sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' |z| > 1 '''* La fonction sinus discret'''
'''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)'''
{{AutoCat}}
mbzvgtu7eky8svyoahsl2w02ixei7ms
Mathc initiation/a588
0
81071
772655
772488
2026-09-21T08:21:12Z
Xhungab
23827
772655
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a583| Sommaire]]
----{{Partie{{{type|}}}|Multiplication par la variable d'évolution : n f(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal x(n)
au lieu du '''signal causale discret x(n) u(n)'''.
'''Le signal''' '''Transformée en Z'''
'''x(n)''' '''X(z)''' '''Z[n x(n)] = -z X'(z)'''
'''u(n) = 1''' '''z/(z-1)''' '''-z [z/(z-1) ]' ''' = '''-z [-1/(z-1)^2]'''
'''r(n) = n''' '''z/(z-1)^2''' '''-z [z/(z-1)^2]' ''' = '''-z [-(z+1)/(z-1)^3]'''
'''c(n) = n^2''' '''z(z+1)/(z-1)^3''' '''-z [z(z+1)/(z-1)^3]' ''' = '''-z [-(z^2+4z+1)/(z-1)^4]'''
'''f(n) = a^n''' '''z/(z-a)''' '''-z [z/(z-a) ]' ''' = '''-z [-a/(z-a)^2]'''
'''Le signal g(n)''' : '''cos(kn)'''
'''Transformée en Z''' : ''' [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''Z[n g(n)]''' : '''-z [[z^2-z cos(k)]/[z^2-2z cos(k)+1]]' '''
'''-z G'(z)''' : '''-z [2z-(z^2+1)cos(k)]/[z^2-2z cos(k)+1]^2'''
'''Le signal h(n)''' : '''sin(kn)'''
'''Transformée en Z''' : ''' [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Z[n h(n)]''' : '''-z [[z sin(k)]/[z^2-2z cos(k)+1]]' '''
'''-z H'(z)''' : '''-z [-[(z^2-1)sin(k)]/[z^2-2z cos(k)+1]^2]'''
{{AutoCat}}
ht7kxwk7gs87zj3bsex40nwnwvrnuu1
772656
772655
2026-09-21T08:21:31Z
Xhungab
23827
772656
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a583| Sommaire]]
----{{Partie{{{type|}}}|Multiplication par la variable d'évolution : n x(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal x(n)
au lieu du '''signal causale discret x(n) u(n)'''.
'''Le signal''' '''Transformée en Z'''
'''x(n)''' '''X(z)''' '''Z[n x(n)] = -z X'(z)'''
'''u(n) = 1''' '''z/(z-1)''' '''-z [z/(z-1) ]' ''' = '''-z [-1/(z-1)^2]'''
'''r(n) = n''' '''z/(z-1)^2''' '''-z [z/(z-1)^2]' ''' = '''-z [-(z+1)/(z-1)^3]'''
'''c(n) = n^2''' '''z(z+1)/(z-1)^3''' '''-z [z(z+1)/(z-1)^3]' ''' = '''-z [-(z^2+4z+1)/(z-1)^4]'''
'''f(n) = a^n''' '''z/(z-a)''' '''-z [z/(z-a) ]' ''' = '''-z [-a/(z-a)^2]'''
'''Le signal g(n)''' : '''cos(kn)'''
'''Transformée en Z''' : ''' [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''Z[n g(n)]''' : '''-z [[z^2-z cos(k)]/[z^2-2z cos(k)+1]]' '''
'''-z G'(z)''' : '''-z [2z-(z^2+1)cos(k)]/[z^2-2z cos(k)+1]^2'''
'''Le signal h(n)''' : '''sin(kn)'''
'''Transformée en Z''' : ''' [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Z[n h(n)]''' : '''-z [[z sin(k)]/[z^2-2z cos(k)+1]]' '''
'''-z H'(z)''' : '''-z [-[(z^2-1)sin(k)]/[z^2-2z cos(k)+1]^2]'''
{{AutoCat}}
i1gigmm2smt5qky8d1ymingdcih6sm6
Mathc initiation/0070
0
84515
772652
772492
2026-09-21T07:48:33Z
Xhungab
23827
772652
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==L'échelon unitaire : u(n) = 1==
'''La transformée en Z de u(n) est : U(z) = z/(z-1)'''
sum u(n) z^(-n), n=0 to infinity u(n) = 1
sum (1) z^(-n), n=0 to infinity
sum (z^(-1))^n, n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (z^(-1))^n, n=0 to infinity = 1/(1-z^(-1)) x = z^(-1)
= 1/(1-1/z) z^(-1) = 1/z
= '''z/(z-1)'''
==La rampe : r(n) = n==
'''La transformée en Z de r(n) est : R(z) = z/(z-1)^2'''
'''Travaillons sur x :'''
sum x^(n), n=0 to infinity = 1/(1-x) |x| < 1
sum x'^(n), n=0 to infinity = (1/(1-x))' Dérivons
sum n x^(n-1), n=0 to infinity = 1/(1-x)^2
(x) [sum n x^(n-1), n=0 to infinity = 1/(1-x)^2] Multiplions par x
(*) sum n x^(n ), n=0 to infinity = x/(1-x)^2
'''Travaillons sur z :''' x -> z^(-1)
sum n (z^(-1))^(n), n=0 to infinity = (*) Remplaçons x par (z^(-1))
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = a) Simplifions (z^(-1))^(n)
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = b) Simplifions (z^(-1)) = (1/z)
(1/z)/(1-(1/z))^2
sum n z^(-n)), n=0 to infinity = c) Développons les (1/z)
1/[((z-1)/z))^2 z]
sum n z^(-n)), n=0 to infinity = d) Distribuons les carrés
1/[(z-1)^2/z^2) z]
sum n z^(-n)), n=0 to infinity = e) Simplifions le z^2
z^2 /[(z-1))^2 z]
sum n z^(-n)), n=0 to infinity = f) Cela donne
'''z/(z-1)^2'''
==Le carré : c(n) = n^2==
'''La transformée en Z de c(n) = n^2 est : C(z) = z(z+1)/(z-1)^3'''
Utilisons la transformé en Z de la rampe r(n) = n R(z) = z/(z-1)^2
Si nous utilisons la propriété de [[Mathc initiation/a588|la Multiplication par la variable d'évolution]] sur la transformé de la rampe nous obtenons :
x(n) = n r(n) = n n = n^2 alors Z[x(n)] = Z[n r(n)] = -z R'(z)
R'(z) = (z/(z-1)^2)' = -(z+1)/(z-1)^3
Z[n r(n)] = (-z) R'(z) = (-z) (-(z+1)/(z-1)^3)
Z[n r(n)] = '''z (z+1)/(z-1)^3 = C(n)'''
==L'exponentiel : f(n) = a^n==
'''La transformée en Z de f(n) est : F(z) = z/(z-a)'''
sum f(n) z^(-n), n=0 to infinity f(n) = a^n
sum (a^n) z^(-n), n=0 to infinity
sum (a^n) (z^(-1))^(n), n=0 to infinity
sum (a z^(-1))^(n), n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (a z^(-1))^(n), n=0 to infinity = 1/(1-(a z^(-1))) Remplaçons x par (a z^(-1))
= 1/(1-(a/z)) (a z^(-1)) = (a/z)
= z/(z-(a))
= '''z/(z- a)'''
{{AutoCat}}
iqxt8q371dm32c00rvwnjbobv8afla3
772653
772652
2026-09-21T07:57:18Z
Xhungab
23827
772653
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==L'échelon unitaire : u(n) = 1==
'''La transformée en Z de u(n) est : U(z) = z/(z-1)'''
sum u(n) z^(-n), n=0 to infinity u(n) = 1
sum (1) z^(-n), n=0 to infinity
sum (z^(-1))^n, n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (z^(-1))^n, n=0 to infinity = 1/(1-z^(-1)) x = z^(-1)
= 1/(1-1/z) z^(-1) = 1/z
= '''z/(z-1)'''
==La rampe : r(n) = n==
'''La transformée en Z de r(n) est : R(z) = z/(z-1)^2'''
'''Travaillons sur x :'''
sum x^(n), n=0 to infinity = 1/(1-x) |x| < 1
sum x'^(n), n=0 to infinity = (1/(1-x))' Dérivons
sum n x^(n-1), n=0 to infinity = 1/(1-x)^2
(x) [sum n x^(n-1), n=0 to infinity = 1/(1-x)^2] Multiplions par x
(*) sum n x^(n ), n=0 to infinity = x/(1-x)^2
'''Travaillons sur z :''' x -> z^(-1)
sum n (z^(-1))^(n), n=0 to infinity = (*) Remplaçons x par (z^(-1))
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = a) Simplifions (z^(-1))^(n)
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = b) Simplifions (z^(-1)) = (1/z)
(1/z)/(1-(1/z))^2
sum n z^(-n)), n=0 to infinity = c) Développons les (1/z)
1/[((z-1)/z))^2 z]
sum n z^(-n)), n=0 to infinity = d) Distribuons les carrés
1/[(z-1)^2/z^2) z]
sum n z^(-n)), n=0 to infinity = e) Simplifions le z^2
z^2 /[(z-1))^2 z]
sum n z^(-n)), n=0 to infinity = f) Cela donne
'''z/(z-1)^2'''
==Le carré : c(n) = n^2==
'''La transformée en Z de c(n) = n^2 est : C(z) = z(z+1)/(z-1)^3'''
Utilisons la transformé en Z de la rampe r(n) = n R(z) = z/(z-1)^2
Si nous utilisons la propriété de [[Mathc initiation/a588|la Multiplication par la variable d'évolution]] sur la transformé de la rampe nous obtenons :
x(n) = n r(n) = n n = n^2 alors Z[x(n)] = Z[n r(n)] = -z R'(z)
R'(z) = (z/(z-1)^2)' = -(z+1)/(z-1)^3
Z[n r(n)] = (-z) R'(z) = (-z) (-(z+1)/(z-1)^3)
Z[n r(n)] = '''z (z+1)/(z-1)^3 = C(n)'''
==L'exponentiel : f(n) = a^n==
'''La transformée en Z de f(n) est : F(z) = z/(z-a)'''
sum f(n) z^(-n), n=0 to infinity f(n) = a^n
sum (a^n) z^(-n), n=0 to infinity
sum (a^n) (z^(-1))^(n), n=0 to infinity
sum (a z^(-1))^(n), n=0 to infinity
sum (a 1/z)^(n), n=0 to infinity z^(-1) = 1/z
sum (a/z)^(n), n=0 to infinity
Remarque * sum x^n, n=0 to infinity = 1/(1-x) |x|<1
* sum (a/z)^(n), n=0 to infinity = 1/(1-(a/z)) Remplaçons x par (a/z)
= z/(z-(a))
= '''z/(z- a)'''
{{AutoCat}}
2seqv81ihrp4bylo92h7ct3hrirs5gl
772654
772653
2026-09-21T07:58:49Z
Xhungab
23827
772654
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==L'échelon unitaire : u(n) = 1==
'''La transformée en Z de u(n) est : U(z) = z/(z-1)'''
sum u(n) z^(-n), n=0 to infinity u(n) = 1
sum (1) z^(-n), n=0 to infinity
sum (z^(-1))^n, n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (z^(-1))^n, n=0 to infinity = 1/(1-z^(-1)) x = z^(-1)
= 1/(1-1/z) z^(-1) = 1/z
= '''z/(z-1)'''
==La rampe : r(n) = n==
'''La transformée en Z de r(n) est : R(z) = z/(z-1)^2'''
'''Travaillons sur x :'''
sum x^(n), n=0 to infinity = 1/(1-x) |x| < 1
sum x'^(n), n=0 to infinity = (1/(1-x))' Dérivons
sum n x^(n-1), n=0 to infinity = 1/(1-x)^2
(x) [sum n x^(n-1), n=0 to infinity = 1/(1-x)^2] Multiplions par x
(*) sum n x^(n ), n=0 to infinity = x/(1-x)^2
'''Travaillons sur z :''' x -> z^(-1)
sum n (z^(-1))^(n), n=0 to infinity = (*) Remplaçons x par (z^(-1))
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = a) Simplifions (z^(-1))^(n)
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = b) Simplifions (z^(-1)) = (1/z)
(1/z)/(1-(1/z))^2
sum n z^(-n)), n=0 to infinity = c) Développons les (1/z)
1/[((z-1)/z))^2 z]
sum n z^(-n)), n=0 to infinity = d) Distribuons les carrés
1/[(z-1)^2/z^2) z]
sum n z^(-n)), n=0 to infinity = e) Simplifions le z^2
z^2 /[(z-1))^2 z]
sum n z^(-n)), n=0 to infinity = f) Cela donne
'''z/(z-1)^2'''
==Le carré : c(n) = n^2==
'''La transformée en Z de c(n) = n^2 est : C(z) = z(z+1)/(z-1)^3'''
Utilisons la transformé en Z de la rampe r(n) = n R(z) = z/(z-1)^2
Si nous utilisons la propriété de [[Mathc initiation/a588|la Multiplication par la variable d'évolution]] sur la transformé de la rampe nous obtenons :
x(n) = n r(n) = n n = n^2 alors Z[x(n)] = Z[n r(n)] = -z R'(z)
R'(z) = (z/(z-1)^2)' = -(z+1)/(z-1)^3
Z[n r(n)] = (-z) R'(z) = (-z) (-(z+1)/(z-1)^3)
Z[n r(n)] = '''z (z+1)/(z-1)^3 = C(n)'''
==L'exponentiel : f(n) = a^n==
'''La transformée en Z de f(n) est : F(z) = z/(z-a)'''
sum f(n) z^(-n), n=0 to infinity f(n) = a^n
sum (a^n) z^(-n), n=0 to infinity
sum (a^n) (z^(-1))^(n), n=0 to infinity
sum (a z^(-1))^(n), n=0 to infinity
sum (a 1/z )^(n), n=0 to infinity z^(-1) = 1/z
sum (a/z)^(n), n=0 to infinity
Remarque * sum x^n, n=0 to infinity = 1/(1-x) |x|<1
* sum (a/z)^(n), n=0 to infinity = 1/(1-(a/z)) Remplaçons x par (a/z)
= z/(z-(a))
= '''z/(z- a)'''
{{AutoCat}}
4r0o30s7pdyc70fp833gozu0g2sh6a1
Mathc initiation/0071
0
84517
772658
2026-09-21T10:05:05Z
Xhungab
23827
news
772658
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==La fonction cosinus discret : g(n) = cos(kn)==
'''La transformée en Z de g[n] est :
G[z] = [z^2 - z cos(k)] / [z^2 - 2 z cos(k) + 1]'''
g[n] = cos(kn) * cos(kn) = (e^(kni)+e^(-kni))/2
g[n] = (e^(kni) + e^(-kni))/2
g[n] = 1/2 (e^(kni)+e^(-kni))
G[z] = 1/2 (z/(z-e^(ki))+z/(z-e^(-ki)) f(n) = a^n F[z] = z/(z-a)
f(n) = e^(kni) F[z] = z/(z-(e^(ki)))
G[z] = z/2 (1/(z-e^(ki))+1/(z-e^(-ki))
G[z] = z/2 ((z-e^(-ki)+(z-e^(ki)) / ((z-e^(ki))(z-e^(-ki)))
'''Numérateur :'''
G[z] = z/2 ((2z- e^(-ki)-e^(ki )) / ...
G[z] = z/2 ((2z-(e^(-ki)+e^(ki))) / ... * e^(ki) +e^(-ki) = 2 cos(k)
G[z] = z/2 ((2z - 2 cos(k)) /...
G[z] = z (( z - cos(k)) /...
G[z] = ( z^2- z cos(k)) /...
'''Dénominateur :'''
G[z] = ... / ( (z - e^(ki)) (z - e^(-ki)) )
G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + e^(ki)(e^(-ki))
G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + 1) e^(ki)(e^(-ki) = 1
G[z] = ... / ( (z^2 - z (e^(-ki) + z e^(ki)) + 1) * e^(ki) + e^(-ki) = 2 cos(k)
G[z] = ... / ( (z^2 - z (2 cos(k)) + 1)
G[z] = ... / (z^2 - 2 z cos(k) + 1)
'''Donc :'''
G[z] = (z^2 - z cos(k) ) / (z^2 - 2 z cos(k) + 1)
{{AutoCat}}
5cnwgjkqeiu2l4bzqolb82yoqjigtyc
772659
772658
2026-09-21T10:31:56Z
Xhungab
23827
772659
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==La fonction cosinus discret : g(n) = cos(kn)==
'''La transformée en Z de g[n] est :
G[z] = [z^2 - z cos(k)] / [z^2 - 2 z cos(k) + 1]'''
g[n] = cos(kn) * cos(kn) = (e^(kni)+e^(-kni))/2
g[n] = (e^(kni) + e^(-kni))/2
g[n] = 1/2 ('''e^(kni)''' + e^(-kni))
f(n) = a^n F[z] = z/(z-a)
G[z] = 1/2 '''(z/(z-e^(ki))''' + z/(z-e^(-ki)) f(n) = '''e^(kni)''' F[z] = '''z/(z-e^(ki))'''
G[z] = '''z'''/2 (1/(z-e^(ki)) + 1/(z-e^(-ki))
Mis au même dénominateur.
G[z] = z/2 ((z-e^(-ki) + (z-e^(ki)) / ((z-e^(ki))(z-e^(-ki)))
G[z] = z/2 ((2z-e^(-ki)-e^(ki)) / ((z-e^(ki))(z-e^(-ki)))
'''Numérateur :'''
G[z] = z/2 ((2z- e^(-ki)-e^(ki )) / ...
G[z] = z/2 ((2z-(e^(-ki)+e^(ki))) / ... * e^(ki)+e^(-ki) = 2 cos(k)
G[z] = z/2 ((2z - 2 cos(k)) /...
G[z] = z (( z - cos(k)) /...
G[z] = ( z^2- z cos(k)) /...
'''Dénominateur :'''
G[z] = ... / ( (z - e^(ki)) (z - e^(-ki)) )
G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + '''e^(ki)(e^(-ki)''')
G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + 1) '''e^(ki)(e^(-ki) = 1'''
G[z] = ... / ( (z^2 - z '''(e^(-ki)+e^(ki))''' + 1) * '''e^(ki) + e^(-ki) = 2 cos(k)'''
G[z] = ... / ( (z^2 - z ('''2 cos(k)''') + 1)
G[z] = ... / (z^2 - 2 z cos(k) + 1)
'''Donc :'''
G[z] = (z^2 - z cos(k) ) / (z^2 - 2 z cos(k) + 1)
{{AutoCat}}
me39jo5eqvsdjh3e2kqzulc4zkax5ui
772660
772659
2026-09-21T10:46:25Z
Xhungab
23827
772660
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==La fonction cosinus discret : g(n) = cos(kn)==
'''La transformée en Z de g[n] est :
G[z] = [z^2 - z cos(k)] / [z^2 - 2 z cos(k) + 1]'''
g[n] = cos(kn) * cos(kn) = (e^(kni)+e^(-kni))/2
g[n] = (e^(kni) + e^(-kni))/2
g[n] = 1/2 ('''e^(kni)''' + e^(-kni))
f(n) = a^n F[z] = z/(z-a)
G[z] = 1/2 '''(z/(z-e^(ki))''' + z/(z-e^(-ki)) f(n) = '''e^(kni)''' F[z] = '''z/(z-e^(ki))'''
G[z] = '''z'''/2 (1/(z-e^(ki)) + 1/(z-e^(-ki))
Mis au même dénominateur.
G[z] = z/2 (('''z'''-e^(-ki) + ('''z'''-e^(ki)) / ((z-e^(ki))(z-e^(-ki)))
G[z] = z/2 ((2z-('''e^(-ki)+e^(ki)''')) / ((z-e^(ki))(z-e^(-ki)))
'''Numérateur :'''
G[z] = z/2 ((2z-('''e^(-ki)+e^(ki))''') / ... * '''e^(ki)+e^(-ki) = 2 cos(k)'''
G[z] = z/2 ((2z - 2 cos(k)) /...
G[z] = z (( z - cos(k)) /...
G[z] = ( z^2- z cos(k)) /...
'''Dénominateur :'''
G[z] = ... / ( (z - e^(ki)) (z - e^(-ki)) ) Développons :
G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + '''e^(ki)(e^(-ki)''')
G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + 1) '''e^(ki)(e^(-ki) = 1'''
G[z] = ... / ( (z^2 - z '''(e^(-ki)+e^(ki))''' + 1) * '''e^(ki)+e^(-ki) = 2 cos(k)'''
G[z] = ... / ( (z^2 - z ('''2 cos(k)''') + 1)
G[z] = ... / (z^2 - 2 z cos(k) + 1)
'''Donc :'''
G[z] = (z^2 - z cos(k) ) / (z^2 - 2 z cos(k) + 1)
{{AutoCat}}
26q6b3atkweih6tyir3zgzjz8sky6gq
772661
772660
2026-09-21T10:48:50Z
Xhungab
23827
772661
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==La fonction cosinus discret : g(n) = cos(kn)==
'''La transformée en Z de g[n] est :
G[z] = [z^2 - z cos(k)] / [z^2 - 2 z cos(k) + 1]'''
g[n] = cos(kn) * cos(kn) = (e^(kni)+e^(-kni))/2
g[n] = (e^(kni) + e^(-kni))/2
g[n] = 1/2 ('''e^(kni)''' + e^(-kni))
f(n) = a^n F[z] = z/(z-a)
G[z] = 1/2 '''(z/(z-e^(ki))''' + z/(z-e^(-ki)) f(n) = '''e^(kni)''' F[z] = '''z/(z-e^(ki))'''
G[z] = '''z'''/2 (1/(z-e^(ki)) + 1/(z-e^(-ki))
Mis au même dénominateur.
G[z] = z/2 (('''z'''-e^(-ki) + ('''z'''-e^(ki)) / ((z-e^(ki))(z-e^(-ki)))
G[z] = z/2 ((2z-('''e^(-ki)+e^(ki)''')) / ((z-e^(ki))(z-e^(-ki)))
'''Numérateur :'''
G[z] = z/2 ((2z-('''e^(-ki)+e^(ki))''') / ... * '''e^(ki)+e^(-ki) = 2 cos(k)'''
G[z] = z/2 ((2z - 2 cos(k)) /...
G[z] = z (( z - cos(k)) /...
G[z] = '''( z^2- z cos(k))''' /...
'''Dénominateur :'''
G[z] = ... / ( (z - e^(ki)) (z - e^(-ki)) ) Développons :
G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + '''e^(ki)(e^(-ki)''')
G[z] = ... / ( (z^2 - z e^(-ki)-z e^(ki) + 1) '''e^(ki)(e^(-ki) = 1'''
G[z] = ... / ( (z^2 - z '''(e^(-ki)+e^(ki))''' + 1) * '''e^(ki)+e^(-ki) = 2 cos(k)'''
G[z] = ... / ( (z^2 - z ('''2 cos(k)''') + 1)
G[z] = ... / ('''z^2 - 2 z cos(k) + 1)'''
'''Donc :'''
G[z] = (z^2 - z cos(k) ) / (z^2 - 2 z cos(k) + 1)
{{AutoCat}}
49942t7p0m2srth3s3oba9fny2cjhko